From 97d19cfcdb20e53f97db6efd2fa2300ce5c56595 Mon Sep 17 00:00:00 2001 From: Steve Hobbs Date: Tue, 15 Oct 2019 13:38:44 +0100 Subject: [PATCH] CredentialsManager function to clear and revoke the refresh token (#312) * Added CredentialsManager.clearAndRevokeToken(), superceding .clear() * Added deprecation notice above the old clear() method * Added an example of `clearAndRevokeToken` to the readme * Fixed unit tests They needed a sprinkling of `waitUntil` blocks so that the tests didn't trip over themselves with clearing credentials asynchronously while other tests were running. * Reverted changes to icons and storyboard * Removed the default case in favour of .success * Bumped timeout values inline with other waitUntil calls May fix timeout issue when running in CI environment * Bumped timeout for clearAndRevoke for multi manager tests * Reversed 'clear' deprecation, reverted tests * Tests have been reverted to a previous state where they used `clear` to remove credentials between tests, now that `clear` is no longer depcreated * Appropriate use of `waitUntil` has been restored into the tests so that they run properly * Renamed clearAndRevokeToken to revoke, updated README * Applied readme changes based on review feedback * Added more detail to doc comments for revoke method * Reworded test spec for clarity --- Auth0/CredentialsManager.swift | 28 ++++++++++ Auth0/CredentialsManagerError.swift | 1 + Auth0Tests/CredentialsManagerSpec.swift | 70 +++++++++++++++++++++++++ README.md | 20 +++++++ 4 files changed, 119 insertions(+) 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.