diff --git a/Auth0.xcodeproj/project.pbxproj b/Auth0.xcodeproj/project.pbxproj index 459be142..2821781b 100644 --- a/Auth0.xcodeproj/project.pbxproj +++ b/Auth0.xcodeproj/project.pbxproj @@ -89,6 +89,10 @@ 5C4F553A23C9125600C89615 /* JWTAlgorithmSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F553923C9125600C89615 /* JWTAlgorithmSpec.swift */; }; 5C53A7E92703A23200A7C0A3 /* UserInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2860D41EEF20F300C75D54 /* UserInfoSpec.swift */; }; 5C53A7EA2703A23300A7C0A3 /* UserInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2860D41EEF20F300C75D54 /* UserInfoSpec.swift */; }; + 5C80980B275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */; }; + 5C80980C275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */; }; + 5C80980D275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */; }; + 5C80980E275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */; }; 5CB41D4023D0BA2C00074024 /* IDTokenValidatorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3C23D0BA2C00074024 /* IDTokenValidatorContext.swift */; }; 5CB41D4423D0BA2C00074024 /* IDTokenSignatureValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3D23D0BA2C00074024 /* IDTokenSignatureValidator.swift */; }; 5CB41D4823D0BA2C00074024 /* IDTokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3E23D0BA2C00074024 /* IDTokenValidator.swift */; }; @@ -461,6 +465,7 @@ 5C4F553423C9124200C89615 /* JWKSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWKSpec.swift; sourceTree = ""; }; 5C4F553923C9125600C89615 /* JWTAlgorithmSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWTAlgorithmSpec.swift; sourceTree = ""; }; 5C60412E27482A2600EEF515 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsStorage.swift; sourceTree = ""; }; 5CB41D3C23D0BA2C00074024 /* IDTokenValidatorContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDTokenValidatorContext.swift; sourceTree = ""; }; 5CB41D3D23D0BA2C00074024 /* IDTokenSignatureValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDTokenSignatureValidator.swift; sourceTree = ""; }; 5CB41D3E23D0BA2C00074024 /* IDTokenValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDTokenValidator.swift; sourceTree = ""; }; @@ -687,6 +692,7 @@ 5B1748731EF2D3A40060E653 /* Date.swift */, 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */, 5BEDE1891EC21B040007300D /* CredentialsManager.swift */, + 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */, 5B5E93F81EC45C22002A37F9 /* CredentialsManagerError.swift */, ); name = Utils; @@ -1557,6 +1563,7 @@ 5F4A1F961D00AABC00C72242 /* OAuth2Grant.swift in Sources */, 5FCAB1731D09009600331C84 /* NSData+URLSafe.swift in Sources */, 5B16D88E1F7141A0009476A5 /* ASTransaction.swift in Sources */, + 5C80980B275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, 5FD255BA1D14F70B00387ECB /* WebAuthError.swift in Sources */, 5BEDE18A1EC21B040007300D /* CredentialsManager.swift in Sources */, 5B2860CE1EEAC30500C75D54 /* UserInfo.swift in Sources */, @@ -1615,6 +1622,7 @@ 5FE2F8B31CCEAED8003628F4 /* Requestable.swift in Sources */, 5B2860CF1EEAC30900C75D54 /* UserInfo.swift in Sources */, 5C4F552423C8FBA100C89615 /* JWKS.swift in Sources */, + 5C80980C275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, 5C41F6C9244F969F00252548 /* ASCallbackTransaction.swift in Sources */, 5C41F6D2244F972B00252548 /* JWTAlgorithm.swift in Sources */, 5C41F6D7244F975A00252548 /* TransactionStore.swift in Sources */, @@ -1755,6 +1763,7 @@ 5FDE875F1D8A424700EA27DC /* AuthenticationError.swift in Sources */, 5B1748761EF2D3A70060E653 /* Date.swift in Sources */, 5F23E6E31D4ACD7F00C3F2D9 /* ManagementError.swift in Sources */, + 5C80980E275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1790,6 +1799,7 @@ 5FDE87601D8A424700EA27DC /* AuthenticationError.swift in Sources */, 5B1748771EF2D3A90060E653 /* Date.swift in Sources */, 5F23E70C1D4B88F600C3F2D9 /* ManagementError.swift in Sources */, + 5C80980D275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2392,6 +2402,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.Auth0; PRODUCT_NAME = Auth0; SDKROOT = watchos; diff --git a/Auth0/Auth0WebAuth.swift b/Auth0/Auth0WebAuth.swift index 01e472f2..527851c2 100644 --- a/Auth0/Auth0WebAuth.swift +++ b/Auth0/Auth0WebAuth.swift @@ -264,4 +264,47 @@ extension Auth0WebAuth { } +// MARK: - Async/Await + +#if compiler(>=5.5) && canImport(_Concurrency) +extension Auth0WebAuth { + + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func start() async throws -> Credentials { + return try await withCheckedThrowingContinuation { continuation in + self.start(continuation.resume) + } + } + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func start() async throws -> Credentials { + return try await withCheckedThrowingContinuation { continuation in + self.start(continuation.resume) + } + } + #endif + + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func clearSession(federated: Bool) async -> Bool { + return await withCheckedContinuation { continuation in + self.clearSession(federated: federated) { result in + continuation.resume(returning: result) + } + } + } + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func clearSession(federated: Bool) async -> Bool { + return await withCheckedContinuation { continuation in + self.clearSession(federated: federated) { result in + continuation.resume(returning: result) + } + } + } + #endif + +} +#endif #endif diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index ed176493..26060051 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -8,41 +8,6 @@ import LocalAuthentication import Combine #endif -/// Generic storage API for storing credentials -public protocol CredentialsStorage { - /// Retrieve a storage entry - /// - /// - Parameters: - /// - forKey: The key to get from the store - /// - Returns: The stored data - func getEntry(forKey: String) -> Data? - - /// Set a storage entry - /// - /// - Parameters: - /// - _: The data to be stored - /// - forKey: The key to store it to - /// - Returns: if credentials were stored - func setEntry(_: Data, forKey: String) -> Bool - - /// Delete a storage entry - /// - /// - Parameters: - /// - forKey: The key to delete from the store - /// - Returns: if credentials were deleted - func deleteEntry(forKey: String) -> Bool -} - -extension A0SimpleKeychain: CredentialsStorage { - public func getEntry(forKey: String) -> Data? { - return data(forKey: forKey) - } - - public func setEntry(_ data: Data, forKey: String) -> Bool { - return setData(data, forKey: forKey) - } -} - /// Credentials management utility public struct CredentialsManager { @@ -343,3 +308,81 @@ public extension CredentialsManager { } } + +// MARK: - Async/Await + +#if compiler(>=5.5) && canImport(_Concurrency) +public extension CredentialsManager { + + /// Calls the revoke token endpoint to revoke the refresh token and, if successful, the credentials are cleared. Otherwise, + /// the credentials are not cleared and an error is thrown. + /// + /// If no refresh token is available the endpoint is not called, the credentials are cleared, and no error is thrown. + /// + /// - Parameter headers: additional headers to add to a possible token revocation. The headers will be set via Request.headers. + /// - Throws: An error of type `CredentialsManagerError`. + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func revoke(headers: [String: String] = [:]) async throws { + return try await withCheckedThrowingContinuation { continuation in + self.revoke(headers: headers) { error in + if let error = error { return continuation.resume(throwing: error) } + continuation.resume(returning: ()) + } + } + } + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func revoke(headers: [String: String] = [:]) async throws { + return try await withCheckedThrowingContinuation { continuation in + self.revoke(headers: headers) { error in + if let error = error { return continuation.resume(throwing: error) } + continuation.resume(returning: ()) + } + } + } + #endif + + /// Retrieve credentials from the keychain and yield new credentials using `refreshToken` if `accessToken` has expired, + /// otherwise return the retrieved credentials as they have not expired. Renewed credentials will + /// be stored in the keychain. + /// + /// ``` + /// let credentials = try await credentialsManager.credentials() + /// ``` + /// + /// - Parameters: + /// - scope: scopes to request for the new tokens. By default is nil which will ask for the same ones requested during original Auth. + /// - minTTL: minimum time in seconds the access token must remain valid to avoid being renewed. + /// - parameters: additional parameters to add to a possible token refresh. The parameters will be set via Request.parameters. + /// - headers: additional headers to add to a possible token refresh. The headers will be set via Request.headers. + /// - Returns: the user's credentials. + /// - Throws: An error of type `CredentialsManagerError`. + /// - Important: This method only works for a refresh token obtained after auth with OAuth 2.0 API Authorization. + /// - Note: [Auth0 Refresh Tokens Docs](https://auth0.com/docs/security/tokens/refresh-tokens) + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func credentials(withScope scope: String? = nil, minTTL: Int = 0, parameters: [String: Any] = [:], headers: [String: String] = [:]) async throws -> Credentials { + return try await withCheckedThrowingContinuation { continuation in + self.credentials(withScope: scope, + minTTL: minTTL, + parameters: parameters, + headers: headers, + callback: continuation.resume) + } + } + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func credentials(withScope scope: String? = nil, minTTL: Int = 0, parameters: [String: Any] = [:], headers: [String: String] = [:]) async throws -> Credentials { + return try await withCheckedThrowingContinuation { continuation in + self.credentials(withScope: scope, + minTTL: minTTL, + parameters: parameters, + headers: headers, + callback: continuation.resume) + } + } + #endif + +} +#endif diff --git a/Auth0/CredentialsStorage.swift b/Auth0/CredentialsStorage.swift new file mode 100644 index 00000000..a0e07ce3 --- /dev/null +++ b/Auth0/CredentialsStorage.swift @@ -0,0 +1,38 @@ +import SimpleKeychain + +/// Generic storage API for storing credentials +public protocol CredentialsStorage { + /// Retrieve a storage entry + /// + /// - Parameters: + /// - forKey: The key to get from the store + /// - Returns: The stored data + func getEntry(forKey: String) -> Data? + + /// Set a storage entry + /// + /// - Parameters: + /// - _: The data to be stored + /// - forKey: The key to store it to + /// - Returns: if credentials were stored + func setEntry(_: Data, forKey: String) -> Bool + + /// Delete a storage entry + /// + /// - Parameters: + /// - forKey: The key to delete from the store + /// - Returns: if credentials were deleted + func deleteEntry(forKey: String) -> Bool +} + +extension A0SimpleKeychain: CredentialsStorage { + + public func getEntry(forKey: String) -> Data? { + return data(forKey: forKey) + } + + public func setEntry(_ data: Data, forKey: String) -> Bool { + return setData(data, forKey: forKey) + } + +} diff --git a/Auth0/NSURLComponents+OAuth2.swift b/Auth0/NSURLComponents+OAuth2.swift index 33150ef4..35bcdfaf 100644 --- a/Auth0/NSURLComponents+OAuth2.swift +++ b/Auth0/NSURLComponents+OAuth2.swift @@ -2,7 +2,8 @@ import Foundation extension URLComponents { - var a0_fragmentValues: [String: String] { + + var fragmentValues: [String: String] { var dict: [String: String] = [:] let items = fragment?.components(separatedBy: "&") items?.forEach { item in @@ -17,10 +18,11 @@ extension URLComponents { return dict } - var a0_queryValues: [String: String] { + var queryValues: [String: String] { var dict: [String: String] = [:] self.queryItems?.forEach { dict[$0.name] = $0.value } return dict } + } #endif diff --git a/Auth0/OAuth2Grant.swift b/Auth0/OAuth2Grant.swift index 7d35bcd7..3b7b3976 100644 --- a/Auth0/OAuth2Grant.swift +++ b/Auth0/OAuth2Grant.swift @@ -1,3 +1,4 @@ +#if WEB_AUTH_PLATFORM import Foundation protocol OAuth2Grant { @@ -95,9 +96,10 @@ struct PKCE: OAuth2Grant { } func values(fromComponents components: URLComponents) -> [String: String] { - var items = components.a0_fragmentValues - components.a0_queryValues.forEach { items[$0] = $1 } + var items = components.fragmentValues + components.queryValues.forEach { items[$0] = $1 } return items } } +#endif diff --git a/Auth0/Request.swift b/Auth0/Request.swift index cf05c382..d26e1e14 100644 --- a/Auth0/Request.swift +++ b/Auth0/Request.swift @@ -58,7 +58,7 @@ public struct Request: Requestable { /** Starts the request to the server. - - parameter callback: called when the request finishes and yield it's result. + - Parameter callback: called when the request finishes and yield it's result. */ public func start(_ callback: @escaping Callback) { let handler = self.handle @@ -114,3 +114,32 @@ public extension Request { } } + +// MARK: - Async/Await + +#if compiler(>=5.5) && canImport(_Concurrency) +public extension Request { + + /** + Starts the request to the server. + + - Throws: An error that conforms to `Auth0APIError`; either an `AuthenticationError` or a `ManagementError`. + */ + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func start() async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + self.start(continuation.resume) + } + } + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func start() async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + self.start(continuation.resume) + } + } + #endif + +} +#endif diff --git a/Auth0/WebAuth.swift b/Auth0/WebAuth.swift index d09cc1b3..108e3b93 100644 --- a/Auth0/WebAuth.swift +++ b/Auth0/WebAuth.swift @@ -137,12 +137,37 @@ public protocol WebAuth: Trackable, Loggable { ``` Any on going WebAuth Auth session will be automatically cancelled when starting a new one, - and it's corresponding callback with be called with a failure result of `AuthenticationError.userCancelled`. + and it's corresponding callback with be called with a failure result of `WebAuthError.userCancelled`. - Parameter callback: callback called with the result of the WebAuth flow. */ func start(_ callback: @escaping (WebAuthResult) -> Void) + #if compiler(>=5.5) && canImport(_Concurrency) + /** + Starts the WebAuth flow. + + ``` + let credentials = try await Auth0 + .webAuth(clientId: clientId, domain: "samples.auth0.com") + .start() + ``` + + Any on going WebAuth Auth session will be automatically cancelled when starting a new one, + and it will throw a `WebAuthError.userCancelled` error. + + - Returns: the result of the WebAuth flow. + - Throws: An error of type `WebAuthError`. + */ + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func start() async throws -> Credentials + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func start() async throws -> Credentials + #endif + #endif + /** Starts the WebAuth flow. @@ -161,7 +186,7 @@ public protocol WebAuth: Trackable, Loggable { ``` Any on going WebAuth Auth session will be automatically cancelled when starting a new one, - and the subscription will complete with a failure result of `AuthenticationError.userCancelled`. + and the subscription will complete with a failure result of `WebAuthError.userCancelled`. - Returns: a type-erased publisher. */ @@ -224,9 +249,41 @@ public protocol WebAuth: Trackable, Loggable { */ @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) func clearSession(federated: Bool) -> AnyPublisher -} -// MARK: - Combine + #if compiler(>=5.5) && canImport(_Concurrency) + /** + Removes Auth0 session and optionally remove the Identity Provider session. + - seeAlso: [Auth0 Logout docs](https://auth0.com/docs/login/logout) + + You will need to ensure that the **Callback URL** has been added + to the **Allowed Logout URLs** section of your application in the [Auth0 Dashboard](https://manage.auth0.com/#/applications/). + + ``` + let result = await Auth0 + .webAuth(clientId: clientId, domain: "samples.auth0.com") + .clearSession() + ``` + + Remove Auth0 session and remove the IdP session: + + ``` + let result = await Auth0 + .webAuth(clientId: clientId, domain: "samples.auth0.com") + .clearSession(federated: true) + ``` + + - Parameter federated: `Bool` to remove the IdP session. Defaults to `false`. + - Returns: bool outcome of the call. + */ + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func clearSession(federated: Bool) async -> Bool + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func clearSession(federated: Bool) async -> Bool + #endif + #endif +} public extension WebAuth { @@ -291,5 +348,43 @@ public extension WebAuth { return self.clearSession(federated: federated) } + #if compiler(>=5.5) && canImport(_Concurrency) + /** + Removes Auth0 session and optionally remove the Identity Provider session. + - seeAlso: [Auth0 Logout docs](https://auth0.com/docs/login/logout) + + You will need to ensure that the **Callback URL** has been added + to the **Allowed Logout URLs** section of your application in the [Auth0 Dashboard](https://manage.auth0.com/#/applications/). + + ``` + let result = await Auth0 + .webAuth(clientId: clientId, domain: "samples.auth0.com") + .clearSession() + ``` + + Remove Auth0 session and remove the IdP session: + + ``` + let result = await Auth0 + .webAuth(clientId: clientId, domain: "samples.auth0.com") + .clearSession(federated: true) + ``` + + - Parameter federated: `Bool` to remove the IdP session. Defaults to `false`. + - Returns: bool outcome of the call. + */ + #if compiler(>=5.5.2) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) + func clearSession(federated: Bool = false) async -> Bool { + return await self.clearSession(federated: federated) + } + #else + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func clearSession(federated: Bool = false) async -> Bool { + return await self.clearSession(federated: federated) + } + #endif + #endif + } #endif diff --git a/Auth0Tests/BaseTransactionSpec.swift b/Auth0Tests/BaseTransactionSpec.swift index 7c260064..3f873ecc 100644 --- a/Auth0Tests/BaseTransactionSpec.swift +++ b/Auth0Tests/BaseTransactionSpec.swift @@ -37,6 +37,9 @@ class BaseTransactionSpec: QuickSpec { logger: nil, callback: callback) result = nil + stub(condition: isHost(Domain.host!)) { _ + in HTTPStubsResponse.init(error: NSError(domain: "com.auth0", code: -99999, userInfo: nil)) + }.name = "YOU SHALL NOT PASS!" stub(condition: isToken(Domain.host!) && hasAtLeast(["code": code, "code_verifier": generator.verifier, "grant_type": "authorization_code", @@ -45,7 +48,7 @@ class BaseTransactionSpec: QuickSpec { } stub(condition: isJWKSPath(Domain.host!)) { _ in jwksResponse() } } - + afterEach { HTTPStubs.removeAllStubs() } diff --git a/Auth0Tests/CredentialsManagerSpec.swift b/Auth0Tests/CredentialsManagerSpec.swift index 92d0e8d5..91d50241 100644 --- a/Auth0Tests/CredentialsManagerSpec.swift +++ b/Auth0Tests/CredentialsManagerSpec.swift @@ -741,7 +741,7 @@ class CredentialsManagerSpec: QuickSpec { credentialsManager = CredentialsManager(authentication: authentication, storage: storage) } - + it("custom keychain should successfully set and clear credentials") { _ = credentialsManager.store(credentials: credentials) expect(storage.data(forKey: "credentials")).toNot(beNil()) @@ -899,5 +899,197 @@ class CredentialsManagerSpec: QuickSpec { } } + #if compiler(>=5.5) && canImport(_Concurrency) + describe("async await") { + + afterEach { + _ = credentialsManager.clear() + } + + context("credentials") { + + it("should return the credentials using the default parameter values") { + let credentialsManager = credentialsManager! + _ = credentialsManager.store(credentials: credentials) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + _ = try await credentialsManager.credentials() + done() + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + _ = try await credentialsManager.credentials() + done() + } + } else { + done() + } + #endif + } + } + + it("should return the credentials using custom parameter values") { + let credentialsManager = credentialsManager! + stub(condition: isToken(Domain) && hasAtLeast(["refresh_token": RefreshToken, "foo": "bar"]) && hasHeader("foo", value: "bar")) { _ in + return authResponse(accessToken: NewAccessToken, idToken: NewIdToken, refreshToken: NewRefreshToken, expiresIn: ExpiresIn) + } + credentials = Credentials(accessToken: AccessToken, tokenType: TokenType, idToken: IdToken, refreshToken: RefreshToken, expiresIn: Date(timeIntervalSinceNow: -ExpiresIn), scope: "openid profile") + _ = credentialsManager.store(credentials: credentials) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + _ = try await credentialsManager.credentials(withScope: "openid profile offline_access", + minTTL: ValidTTL, + parameters: ["foo": "bar"], + headers: ["foo": "bar"]) + done() + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + _ = try await credentialsManager.credentials(withScope: "openid profile offline_access", + minTTL: ValidTTL, + parameters: ["foo": "bar"], + headers: ["foo": "bar"]) + done() + } + } else { + done() + } + #endif + } + } + + it("should throw an error") { + let credentialsManager = credentialsManager! + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + do { + _ = try await credentialsManager.credentials() + } catch { + done() + } + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + do { + _ = try await credentialsManager.credentials() + } catch { + done() + } + } + } else { + done() + } + #endif + } + } + + } + + context("revoke") { + + it("should revoke using the default parameter values") { + let credentialsManager = credentialsManager! + stub(condition: isRevokeToken(Domain) && hasAtLeast(["token": RefreshToken])) { _ in + return revokeTokenResponse() + } + _ = credentialsManager.store(credentials: credentials) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + _ = try await credentialsManager.revoke() + done() + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + _ = try await credentialsManager.revoke() + done() + } + } else { + done() + } + #endif + } + } + + it("should revoke using custom parameter values") { + let credentialsManager = credentialsManager! + stub(condition: isRevokeToken(Domain) && hasAtLeast(["token": RefreshToken]) && hasHeader("foo", value: "bar")) { _ in + return revokeTokenResponse() + } + _ = credentialsManager.store(credentials: credentials) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + _ = try await credentialsManager.revoke(headers: ["foo": "bar"]) + done() + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + _ = try await credentialsManager.revoke(headers: ["foo": "bar"]) + done() + } + } else { + done() + } + #endif + } + } + + it("should throw an error") { + let credentialsManager = credentialsManager! + stub(condition: isRevokeToken(Domain) && hasAtLeast(["token": RefreshToken])) { _ in + return authFailure() + } + _ = credentialsManager.store(credentials: credentials) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + do { + _ = try await credentialsManager.revoke() + } catch { + done() + } + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + do { + _ = try await credentialsManager.revoke() + } catch { + done() + } + } + } else { + done() + } + #endif + } + } + + } + + } + #endif + } } diff --git a/Auth0Tests/OAuth2GrantSpec.swift b/Auth0Tests/OAuth2GrantSpec.swift index 018ef038..c2da22d5 100644 --- a/Auth0Tests/OAuth2GrantSpec.swift +++ b/Auth0Tests/OAuth2GrantSpec.swift @@ -18,6 +18,16 @@ class OAuth2GrantSpec: QuickSpec { let issuer = "\(domain.absoluteString)/" let leeway = 60 * 1000 + beforeEach { + stub(condition: isHost(domain.host!)) { _ in + return HTTPStubsResponse.init(error: NSError(domain: "com.auth0", code: -99999, userInfo: nil)) + }.name = "YOU SHALL NOT PASS!" + } + + afterEach { + HTTPStubs.removeAllStubs() + } + describe("Authorization Code w/PKCE") { let method = "S256" @@ -32,12 +42,6 @@ class OAuth2GrantSpec: QuickSpec { pkce = PKCE(authentication: authentication, redirectURL: redirectURL, verifier: verifier, challenge: challenge, method: method, issuer: issuer, leeway: leeway, nonce: nil) } - afterEach { - HTTPStubs.removeAllStubs() - stub(condition: isHost(domain.host!)) { _ in - return HTTPStubsResponse.init(error: NSError(domain: "com.auth0", code: -99999, userInfo: nil)) - }.name = "YOU SHALL NOT PASS!" - } it("shoud build credentials") { let token = UUID().uuidString diff --git a/Auth0Tests/RequestSpec.swift b/Auth0Tests/RequestSpec.swift index 15985625..c1ba6ca9 100644 --- a/Auth0Tests/RequestSpec.swift +++ b/Auth0Tests/RequestSpec.swift @@ -155,5 +155,71 @@ class RequestSpec: QuickSpec { } } + #if compiler(>=5.5) && canImport(_Concurrency) + describe("async await") { + + it("should return the response") { + stub(condition: isHost(Url.host!)) { _ in + return HTTPStubsResponse(jsonObject: ["foo": "bar"], statusCode: 200, headers: nil) + } + let request = Request(session: URLSession.shared, url: Url, method: "GET", handle: plainJson, logger: nil, telemetry: Telemetry()) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + let response = try await request.start() + expect(response).toNot(beEmpty()) + done() + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + let response = try await request.start() + expect(response).toNot(beEmpty()) + done() + } + } else { + done() + } + #endif + } + } + + it("should throw an error") { + stub(condition: isHost(Url.host!)) { _ in + return authFailure() + } + let request = Request(session: URLSession.shared, url: Url, method: "GET", handle: plainJson, logger: nil, telemetry: Telemetry()) + waitUntil(timeout: Timeout) { done in + #if compiler(>=5.5.2) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { + Task.init { + do { + _ = try await request.start() + } catch { + done() + } + } + } + #else + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + Task.init { + do { + _ = try await request.start() + } catch { + done() + } + } + } else { + done() + } + #endif + } + } + + } + #endif + } } diff --git a/Auth0Tests/UsersSpec.swift b/Auth0Tests/UsersSpec.swift index 2d8e2c91..d379d18c 100644 --- a/Auth0Tests/UsersSpec.swift +++ b/Auth0Tests/UsersSpec.swift @@ -19,10 +19,14 @@ class UsersSpec: QuickSpec { let users = Auth0.users(token: Token, domain: Domain) + beforeEach { + stub(condition: isHost(Domain)) { _ + in HTTPStubsResponse.init(error: NSError(domain: "com.auth0", code: -99999, userInfo: nil)) + }.name = "YOU SHALL NOT PASS!" + } + afterEach { HTTPStubs.removeAllStubs() - stub(condition: isHost(Domain)) { _ in HTTPStubsResponse.init(error: NSError(domain: "com.auth0", code: -99999, userInfo: nil)) } - .name = "YOU SHALL NOT PASS!" } describe("GET /users/:identifier") { diff --git a/Package.swift b/Package.swift index 50edb7d2..db7ade0f 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription -#if swift(>=5.5) +#if compiler(>=5.5) let webAuthPlatforms: [Platform] = [.iOS, .macOS, .macCatalyst] #else let webAuthPlatforms: [Platform] = [.iOS, .macOS] diff --git a/README.md b/README.md index 096a932b..224c1ade 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,21 @@ Auth0 ``` +
+ Using Async/Await + +```swift +do { + let credentials = try await Auth0 + .webAuth() + .start() + print("Obtained credentials: \(credentials)") +} catch { + print("Failed with \(error)") +} +``` +
+ ### Configuration In order to use Auth0 you need to provide your Auth0 **ClientId** and **Domain**. @@ -200,8 +215,8 @@ Auth0 .userInfo(withAccessToken: accessToken) .start { result in switch result { - case .success(let profile): - print("User Profile: \(profile)") + case .success(let user): + print("User: \(user)") case .failure(let error): print("Failed with \(error)") } @@ -220,13 +235,29 @@ Auth0 if case .failure(let error) = completion { print("Failed with \(error)") } - }, receiveValue: { profile in - print("User Profile: \(profile)") + }, receiveValue: { user in + print("User: \(user)") }) .store(in: &cancellables) ``` +
+ Using Async/Await + +```swift +do { + let user = try await Auth0 + .authentication() + .userInfo(withAccessToken: accessToken) + .start() + print("User: \(user)") +} catch { + print("Failed with \(error)") +} +``` +
+ #### Renew user credentials Use a [Refresh Token](https://auth0.com/docs/tokens/refresh-tokens) to renew user credentials. It's recommended that you read and understand the refresh token process before implementing. @@ -264,6 +295,22 @@ Auth0 ``` +
+ Using Async/Await + +```swift +do { + let credentials = try await Auth0 + .authentication() + .renew(withRefreshToken: refreshToken) + .start() + print("Obtained new credentials: \(credentials)") +} catch { + print("Failed with \(error)") +} +``` +
+ #### Signup with Universal Login You can make users land directly on the Signup page instead of the Login page by specifying the `"screen_hint": "signup"` parameter when performing Web Authentication. Note that this can be combined with `"prompt": "login"`, which indicates whether you want to always show the authentication page or you want to skip if there's an existing session. @@ -367,6 +414,19 @@ credentialsManager ``` +
+ Using Async/Await + +```swift +do { + let credentials = try await credentialsManager.credentials() + print("Obtained credentials: \(credentials)") +} catch { + print("Failed with \(error)") +} +``` +
+ #### Clearing credentials and revoking refresh tokens Credentials can be cleared by using the `clear` function, which clears credentials from the Keychain. @@ -403,6 +463,19 @@ credentialsManager ``` +
+ Using Async/Await + +```swift +do { + try await credentialsManager.revoke() + print("Success") +} catch { + print("Failed with \(error)") +} +``` +
+ #### Biometric authentication You can enable an additional level of user authentication before retrieving credentials using the biometric authentication supported by your device e.g. Face ID or Touch ID. @@ -546,6 +619,47 @@ Auth0 > This requires `Password` Grant or `http://auth0.com/oauth/grant-type/password-realm`. +
+ Using Combine + +```swift +Auth0 + .authentication() + .login(usernameOrEmail: "support@auth0.com", + password: "secret-password", + realm: "Username-Password-Authentication", + scope: "openid profile") + .publisher() + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Failed with \(error)") + } + }, receiveValue: { credentials in + print("Obtained credentials: \(credentials)") + }) + .store(in: &cancellables) +``` +
+ +
+ Using Async/Await + +```swift +do { + let credentials = try await Auth0 + .authentication() + .login(usernameOrEmail: "support@auth0.com", + password: "secret-password", + realm: "Username-Password-Authentication", + scope: "openid profile") + .start() + print("Obtained credentials: \(credentials)") +} catch { + print("Failed with \(error)") +} +``` +
+ #### Sign up with database connection ```swift @@ -558,13 +672,54 @@ Auth0 .start { result in switch result { case .success(let user): - print("User Signed up: \(user)") + print("User signed up: \(user)") case .failure(let error): print("Failed with \(error)") } } ``` +
+ Using Combine + +```swift +Auth0 + .authentication() + .createUser(email: "support@auth0.com", + password: "secret-password", + connection: "Username-Password-Authentication", + userMetadata: ["first_name": "First", "last_name": "Last"]) + .publisher() + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Failed with \(error)") + } + }, receiveValue: { user in + print("User signed up: \(user)") + }) + .store(in: &cancellables) +``` +
+ +
+ Using Async/Await + +```swift +do { + let user = try await Auth0 + .authentication() + .createUser(email: "support@auth0.com", + password: "secret-password", + connection: "Username-Password-Authentication", + userMetadata: ["first_name": "First", "last_name": "Last"]) + .start() + print("User signed up: \(user)") +} catch { + print("Failed with \(error)") +} +``` +
+ ### Management API (Users) You can request more information about a user's profile and manage the user's metadata by accessing the Auth0 [Management API](https://auth0.com/docs/api/management/v2). For security reasons native mobile applications are restricted to a subset of User based functionality. @@ -579,14 +734,49 @@ Auth0 .link("user identifier", withOtherUserToken: "another user token") .start { result in switch result { - case .success(let userInfo): - print("User: \(userInfo)") + case .success(let user): + print("User: \(user)") case .failure(let error): print("Failed with \(error)") } } ``` +
+ Using Combine + +```swift +Auth0 + .users(token: idToken) + .link("user identifier", withOtherUserToken: "another user token") + .publisher() + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Failed with \(error)") + } + }, receiveValue: { user in + print("User: \(user)") + }) + .store(in: &cancellables) +``` +
+ +
+ Using Async/Await + +```swift +do { + let user = try await Auth0 + .users(token: idToken) + .link("user identifier", withOtherUserToken: "another user token") + .start() + print("User: \(user)") +} catch { + print("Failed with \(error)") +} +``` +
+ ### Custom Domains If you are using [Custom Domains](https://auth0.com/docs/custom-domains) and need to call an Auth0 endpoint