diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index 1b024262..a4eaae2c 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -88,6 +88,34 @@ public struct CredentialsManager { return self.storage.deleteEntry(forKey: storeKey) } + /// 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 raised through the callback. + /// + /// If no refresh token is available the endpoint is not called, the credentials are cleared, and the callback is invoked without an error. + /// + /// - Parameter callback: callback with an error if the refresh token could not be revoked + public func revoke(_ callback: @escaping (CredentialsManagerError?) -> Void) { + guard + let data = self.storage.data(forKey: self.storeKey), + let credentials = NSKeyedUnarchiver.unarchiveObject(with: data) as? Credentials, + let refreshToken = credentials.refreshToken else { + _ = self.clear() + return callback(nil) + } + + self.authentication + .revoke(refreshToken: refreshToken) + .start { result in + switch result { + case .failure(let error): + callback(CredentialsManagerError.revokeFailed(error)) + case .success: + _ = self.clear() + callback(nil) + } + } + } + /// Checks if a non-expired set of credentials are stored /// /// - Returns: if there are valid and non-expired credentials stored diff --git a/Auth0/CredentialsManagerError.swift b/Auth0/CredentialsManagerError.swift index 7495c93e..8a78fc3a 100644 --- a/Auth0/CredentialsManagerError.swift +++ b/Auth0/CredentialsManagerError.swift @@ -27,4 +27,5 @@ public enum CredentialsManagerError: Error { case noRefreshToken case failedRefresh(Error) case touchFailed(Error) + case revokeFailed(Error) } diff --git a/Auth0Tests/CredentialsManagerSpec.swift b/Auth0Tests/CredentialsManagerSpec.swift index 78cb99da..7a1d611f 100644 --- a/Auth0Tests/CredentialsManagerSpec.swift +++ b/Auth0Tests/CredentialsManagerSpec.swift @@ -73,6 +73,76 @@ class CredentialsManagerSpec: QuickSpec { } } + describe("clearing and revoking refresh token") { + + beforeEach { + _ = credentialsManager.store(credentials: credentials) + + stub(condition: isRevokeToken(Domain) && hasAtLeast(["token": RefreshToken])) { _ in return revokeTokenResponse() }.name = "revoke success" + } + + afterEach { + _ = credentialsManager.clear() + } + + it("should clear credentials and revoke the refresh token") { + waitUntil(timeout: 2) { done in + credentialsManager.revoke { + expect($0).to(beNil()) + expect(credentialsManager.hasValid()).to(beFalse()) + done() + } + } + } + + it("should not return an error if there were no credentials stored") { + _ = credentialsManager.clear() + + waitUntil(timeout: 2) { done in + credentialsManager.revoke { + expect($0).to(beNil()) + expect(credentialsManager.hasValid()).to(beFalse()) + done() + } + } + } + + it("should not return an error if there is no refresh token, and clear credentials anyway") { + let credentials = Credentials( + accessToken: AccessToken, + idToken: IdToken, + expiresIn: Date(timeIntervalSinceNow: ExpiresIn) + ) + + _ = credentialsManager.store(credentials: credentials) + + waitUntil(timeout: 2) { done in + credentialsManager.revoke { + expect($0).to(beNil()) + expect(credentialsManager.hasValid()).to(beFalse()) + done() + } + } + } + + it("should return the failure if the token could not be revoked, and not clear credentials") { + stub(condition: isRevokeToken(Domain) && hasAtLeast(["token": RefreshToken])) { _ in + return authFailure(code: "400", description: "Revoke failed") + } + + waitUntil(timeout: 2) { done in + credentialsManager.revoke { + expect($0).to(matchError( + CredentialsManagerError.revokeFailed(AuthenticationError(string: "Revoke failed", statusCode: 400)) + )) + + expect(credentialsManager.hasValid()).to(beTrue()) + done() + } + } + } + } + describe("multi instances of credentials manager") { var secondaryCredentialsManager: CredentialsManager! diff --git a/README.md b/README.md index 94bd504c..004f1408 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,26 @@ credentialsManager.credentials { error, credentials in } ``` +#### Clearing credentials and revoking refresh tokens + +Credentials can be cleared by using the `clear` function, which clears credentials from the keychain: + +```swift +let didClear = credentialsManager.clear() +``` + +In addition, credentials can be cleared and the refresh token revoked using a single call to `revoke`. This function will attempt to revoke the current refresh token stored by the credential manager and then clear credentials from the keychain. If revoking the token results in an error, then the credentials are not cleared: + +```swift +credentialsManager.revoke { error in + guard error == nil else { + return print("Failed to revoke refresh token: \(error)") + } + + print("Success") +} +``` + #### 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.