diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..d98f53e5 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,2 @@ +disabled_rules: + - trailing_whitespace \ No newline at end of file diff --git a/Sources/Entities/Issuance/AuthorizedRequest.swift b/Sources/Entities/Issuance/AuthorizedRequest.swift index 4db9c5bf..44e68c07 100644 --- a/Sources/Entities/Issuance/AuthorizedRequest.swift +++ b/Sources/Entities/Issuance/AuthorizedRequest.swift @@ -25,13 +25,13 @@ public extension CanExpire { if issued >= at { return true } - + guard let expiresIn = expiresIn else { return false } - + let expiration = issued + expiresIn - return expiration <= at + return expiration >= at } } @@ -51,14 +51,14 @@ public enum AuthorizedRequest { timeStamp: TimeInterval, dPopNonce: Nonce? ) - - public func isAccessTokenExpired(clock: TimeInterval) -> Bool { + + public func isAccessTokenExpired(_ from: TimeInterval) -> Bool { guard let timeStamp = self.timeStamp else { return true } - return accessToken?.isExpired(issued: timeStamp, at: clock) ?? false + return accessToken?.isExpired(issued: timeStamp, at: from) ?? false } - + public func isRefreshTokenExpired(clock: TimeInterval) -> Bool { guard let timeStamp = self.timeStamp else { return true @@ -66,9 +66,9 @@ public enum AuthorizedRequest { return accessToken?.isExpired( issued: timeStamp, at: clock - ) ?? false + ) ?? false } - + public var timeStamp: TimeInterval? { switch self { case .noProofRequired(_, _, _, let timeStamp, _): @@ -86,7 +86,7 @@ public enum AuthorizedRequest { return dPopNonce } } - + public var noProofToken: IssuanceAccessToken? { switch self { case .noProofRequired(let accessToken, _, _, _, _): @@ -95,7 +95,7 @@ public enum AuthorizedRequest { return nil } } - + public var proofToken: IssuanceAccessToken? { switch self { case .noProofRequired: @@ -104,6 +104,15 @@ public enum AuthorizedRequest { return accessToken } } + + public var refreshToken: IssuanceRefreshToken? { + switch self { + case .noProofRequired(_, let refreshToken, _, _, _): + return refreshToken + case .proofRequired(_, let refreshToken, _, _, _, _): + return refreshToken + } + } } public extension AuthorizedRequest { @@ -132,3 +141,34 @@ public extension AuthorizedRequest { } } } + +extension AuthorizedRequest { + /// Returns a copy of the current `AuthorizedRequest`, replacing the `accessToken` and `timeStamp` + /// - Parameters: + /// - newAccessToken: The new `IssuanceAccessToken` to use. + /// - newTimeStamp: The new `TimeInterval` to use. + /// - Returns: A new `AuthorizedRequest` instance with the updated values. + func replacing(accessToken newAccessToken: IssuanceAccessToken, timeStamp newTimeStamp: TimeInterval) -> AuthorizedRequest { + switch self { + case let .noProofRequired(_, refreshToken, credentialIdentifiers, _, dPopNonce): + return .noProofRequired( + accessToken: newAccessToken, + refreshToken: refreshToken, + credentialIdentifiers: credentialIdentifiers, + timeStamp: newTimeStamp, + dPopNonce: dPopNonce + ) + + case let .proofRequired(_, refreshToken, cNonce, credentialIdentifiers, _, dPopNonce): + return .proofRequired( + accessToken: newAccessToken, + refreshToken: refreshToken, + cNonce: cNonce, + credentialIdentifiers: credentialIdentifiers, + timeStamp: newTimeStamp, + dPopNonce: dPopNonce + ) + } + } +} + diff --git a/Sources/Entities/IssuanceRefreshToken.swift b/Sources/Entities/IssuanceRefreshToken.swift index f80577a8..90ca5fd6 100644 --- a/Sources/Entities/IssuanceRefreshToken.swift +++ b/Sources/Entities/IssuanceRefreshToken.swift @@ -20,8 +20,9 @@ public struct IssuanceRefreshToken: Codable, CanExpire { public let refreshToken: String? - public init(refreshToken: String?) throws { + public init(refreshToken: String?, expiresIn: TimeInterval? = nil) throws { self.refreshToken = refreshToken + self.expiresIn = expiresIn } } diff --git a/Sources/Extensions/Int+Extensions.swift b/Sources/Extensions/Int+Extensions.swift index 683d1890..3da605e9 100644 --- a/Sources/Extensions/Int+Extensions.swift +++ b/Sources/Extensions/Int+Extensions.swift @@ -19,4 +19,9 @@ public extension Int { func isWithinRange(_ range: ClosedRange) -> Bool { return range.contains(self) } + + /// Converts an `Int` to a `TimeInterval` (Double). + var asTimeInterval: TimeInterval { + return TimeInterval(self) + } } diff --git a/Sources/Issuers/Issuer.swift b/Sources/Issuers/Issuer.swift index 373cf602..1becb61c 100644 --- a/Sources/Issuers/Issuer.swift +++ b/Sources/Issuers/Issuer.swift @@ -64,6 +64,12 @@ public protocol IssuerType { notificationId: NotificationObject, dPopNonce: Nonce? ) async throws -> Result + + func refresh( + clientId: String, + authorizedRequest: AuthorizedRequest, + dPopNonce: Nonce? + ) async -> Result } public actor Issuer: IssuerType { @@ -241,17 +247,19 @@ public actor Issuer: IssuerType { switch response { case .success( - (let accessToken, let nonce, let identifiers, let expiresIn, let dPopNonce) + (let accessToken, let refreshToken, let nonce, let identifiers, let expiresIn, let dPopNonce) ): if let cNonce = nonce { return .success( .proofRequired( - accessToken: try IssuanceAccessToken( + accessToken: try .init( accessToken: accessToken.accessToken, tokenType: accessToken.tokenType, - expiresIn: TimeInterval(expiresIn ?? .zero) + expiresIn: expiresIn?.asTimeInterval ?? .zero + ), + refreshToken: try .init( + refreshToken: refreshToken.refreshToken ), - refreshToken: nil, cNonce: cNonce, credentialIdentifiers: identifiers, timeStamp: Date().timeIntervalSinceReferenceDate, @@ -264,9 +272,11 @@ public actor Issuer: IssuerType { accessToken: try IssuanceAccessToken( accessToken: accessToken.accessToken, tokenType: accessToken.tokenType, - expiresIn: TimeInterval(expiresIn ?? .zero) + expiresIn: expiresIn?.asTimeInterval ?? .zero + ), + refreshToken: try .init( + refreshToken: refreshToken.refreshToken ), - refreshToken: nil, credentialIdentifiers: identifiers, timeStamp: Date().timeIntervalSinceReferenceDate, dPopNonce: dPopNonce @@ -307,6 +317,7 @@ public actor Issuer: IssuerType { let response: ( accessToken: IssuanceAccessToken, + refreshToken: IssuanceRefreshToken, nonce: CNonce?, identifiers: AuthorizationDetailsIdentifiers?, tokenType: TokenType?, @@ -323,12 +334,14 @@ public actor Issuer: IssuerType { if let cNonce = response.nonce { return .success( .proofRequired( - accessToken: try IssuanceAccessToken( + accessToken: try .init( accessToken: response.accessToken.accessToken, tokenType: response.tokenType, expiresIn: TimeInterval(response.expiresIn ?? .zero) ), - refreshToken: nil, + refreshToken: try .init( + refreshToken: response.refreshToken.refreshToken + ), cNonce: cNonce, credentialIdentifiers: response.identifiers, timeStamp: Date().timeIntervalSinceReferenceDate, @@ -338,12 +351,14 @@ public actor Issuer: IssuerType { } else { return .success( .noProofRequired( - accessToken: try IssuanceAccessToken( + accessToken: try .init( accessToken: response.accessToken.accessToken, tokenType: response.tokenType, expiresIn: TimeInterval(response.expiresIn ?? .zero) ), - refreshToken: nil, + refreshToken: try .init( + refreshToken: response.refreshToken.refreshToken + ), credentialIdentifiers: response.identifiers, timeStamp: Date().timeIntervalSinceReferenceDate, dPopNonce: response.dPopNonce @@ -838,4 +853,35 @@ public extension Issuer { dPopNonce: dPopNonce ) } + + func refresh( + clientId: String, + authorizedRequest: AuthorizedRequest, + dPopNonce: Nonce? = nil + ) async -> Result { + + if let refreshToken = authorizedRequest.refreshToken { + do { + let token = try await authorizer.refreshAccessToken( + clientId: clientId, + refreshToken: refreshToken, + dpopNonce: dPopNonce, + retry: true + ) + switch token { + case .success((let accessToken, _, _, _, let timeStamp, _)): + return .success(authorizedRequest.replacing( + accessToken: accessToken, + timeStamp: timeStamp?.asTimeInterval ?? .zero + ) + ) + case .failure(let error): + return .failure(error) + } + } catch { + return .failure(error) + } + } + return .success(authorizedRequest) + } } diff --git a/Sources/Main/Authorisers/AuthorizationServerClient.swift b/Sources/Main/Authorisers/AuthorizationServerClient.swift index 035b2906..1c77a403 100644 --- a/Sources/Main/Authorisers/AuthorizationServerClient.swift +++ b/Sources/Main/Authorisers/AuthorizationServerClient.swift @@ -54,6 +54,7 @@ public protocol AuthorizationServerClientType { retry: Bool ) async throws -> Result<( IssuanceAccessToken, + IssuanceRefreshToken, CNonce?, AuthorizationDetailsIdentifiers?, TokenType?, @@ -71,10 +72,25 @@ public protocol AuthorizationServerClientType { retry: Bool ) async throws -> Result<( IssuanceAccessToken, + IssuanceRefreshToken, CNonce?, AuthorizationDetailsIdentifiers?, Int?, Nonce?), Error> + + func refreshAccessToken( + clientId: String, + refreshToken: IssuanceRefreshToken, + dpopNonce: Nonce?, + retry: Bool + ) async throws -> Result<( + IssuanceAccessToken, + CNonce?, + AuthorizationDetailsIdentifiers?, + TokenType?, + Int?, + Nonce? + ), Error> } public actor AuthorizationServerClient: AuthorizationServerClientType { @@ -313,6 +329,7 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { retry: Bool ) async throws -> Result<( IssuanceAccessToken, + IssuanceRefreshToken, CNonce?, AuthorizationDetailsIdentifiers?, TokenType?, @@ -339,7 +356,7 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { ) switch response.body { - case .success(let tokenType, let accessToken, _, let expiresIn, _, let nonce, _, let identifiers): + case .success(let tokenType, let accessToken, let refreshToken, let expiresIn, _, let nonce, _, let identifiers): return .success( ( try .init( @@ -348,6 +365,9 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { value: tokenType ) ), + try .init( + refreshToken: refreshToken + ), .init( value: nonce ), @@ -389,27 +409,25 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { } } - public func requestAccessTokenPreAuthFlow( - preAuthorizedCode: String, - txCode: TxCode?, + public func refreshAccessToken( clientId: String, - transactionCode: String?, - identifiers: [CredentialConfigurationIdentifier], + refreshToken: IssuanceRefreshToken, dpopNonce: Nonce?, retry: Bool ) async throws -> Result<( IssuanceAccessToken, CNonce?, AuthorizationDetailsIdentifiers?, + TokenType?, Int?, - Nonce?), Error> { - let parameters: JSON = try await preAuthCodeFlow( - preAuthorizedCode: preAuthorizedCode, - txCode: txCode, - clientId: clientId, - transactionCode: transactionCode, - identifiers: identifiers - ) + Nonce? + ), Error> { + + let parameters: JSON = JSON([ + Constants.CLIENT_ID_PARAM: clientId, + Constants.GRANT_TYPE_PARAM: Constants.REFRESH_TOKEN, + Constants.REFRESH_TOKEN_PARAM: refreshToken.refreshToken + ].compactMapValues { $0 }) do { let response: ResponseWithHeaders = try await service.formPost( @@ -422,21 +440,34 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { ) switch response.body { - case .success(let tokenType, let accessToken, _, let expiresIn, _, let nonce, _, let identifiers): + case .success( + let tokenType, + let accessToken, + _, + let expiresIn, + _, + let nonce, + _, + let identifiers + ): return .success( ( try .init( accessToken: accessToken, tokenType: .init( value: tokenType - ) + ), + expiresIn: TimeInterval(expiresIn) ), .init( value: nonce ), identifiers, + TokenType( + value: tokenType + ), expiresIn, - dpopNonce + response.dpopNonce() ) ) case .failure(let error, let errorDescription): @@ -450,14 +481,12 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { switch postError { case .useDpopNonce(let nonce): if retry { - return try await requestAccessTokenPreAuthFlow( - preAuthorizedCode: preAuthorizedCode, - txCode: txCode, + return try await refreshAccessToken( clientId: clientId, - transactionCode: transactionCode, - identifiers: identifiers, + refreshToken: refreshToken, dpopNonce: nonce, - retry: false) + retry: false + ) } else { return .failure(ValidationError.retryFailedAfterDpopNonce) } @@ -470,6 +499,91 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { } } + public func requestAccessTokenPreAuthFlow( + preAuthorizedCode: String, + txCode: TxCode?, + clientId: String, + transactionCode: String?, + identifiers: [CredentialConfigurationIdentifier], + dpopNonce: Nonce?, + retry: Bool + ) async throws -> Result<( + IssuanceAccessToken, + IssuanceRefreshToken, + CNonce?, + AuthorizationDetailsIdentifiers?, + Int?, + Nonce?), Error> { + let parameters: JSON = try await preAuthCodeFlow( + preAuthorizedCode: preAuthorizedCode, + txCode: txCode, + clientId: clientId, + transactionCode: transactionCode, + identifiers: identifiers + ) + + do { + let response: ResponseWithHeaders = try await service.formPost( + poster: tokenPoster, + url: tokenEndpoint, + headers: try tokenEndPointHeaders( + dpopNonce: dpopNonce + ), + parameters: parameters.toDictionary().convertToDictionaryOfStrings() + ) + + switch response.body { + case .success(let tokenType, let accessToken, let refreshToken, let expiresIn, _, let nonce, _, let identifiers): + return .success( + ( + try .init( + accessToken: accessToken, + tokenType: .init( + value: tokenType + ) + ), + try .init( + refreshToken: refreshToken + ), + .init( + value: nonce + ), + identifiers, + expiresIn, + dpopNonce + ) + ) + case .failure(let error, let errorDescription): + throw CredentialIssuanceError.pushedAuthorizationRequestFailed( + error: error, + errorDescription: errorDescription + ) + } + } catch { + if let postError = error as? PostError { + switch postError { + case .useDpopNonce(let nonce): + if retry { + return try await requestAccessTokenPreAuthFlow( + preAuthorizedCode: preAuthorizedCode, + txCode: txCode, + clientId: clientId, + transactionCode: transactionCode, + identifiers: identifiers, + dpopNonce: nonce, + retry: false) + } else { + return .failure(ValidationError.retryFailedAfterDpopNonce) + } + default: + return .failure(error) + } + } else { + return .failure(error) + } + } + } + func toAuthorizationDetail( credentialConfigurationIds: [CredentialConfigurationIdentifier] ) -> [AuthorizationDetail] { diff --git a/Sources/Utilities/Constants.swift b/Sources/Utilities/Constants.swift index a164133a..be7cc69e 100644 --- a/Sources/Utilities/Constants.swift +++ b/Sources/Utilities/Constants.swift @@ -34,4 +34,7 @@ public struct Constants { public static let DPOP_NONCE_HEADER = "DPoP-Nonce" public static let USE_DPOP_NONCE = "use_dpop_nonce" + + public static let REFRESH_TOKEN = "refresh_token" + public static let REFRESH_TOKEN_PARAM = "refresh_token" }