Skip to content

Commit

Permalink
CredentialsManager function to clear and revoke the refresh token (#312)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Steve Hobbs committed Oct 15, 2019
1 parent 8cf77c3 commit 97d19cf
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 0 deletions.
28 changes: 28 additions & 0 deletions Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Auth0/CredentialsManagerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ public enum CredentialsManagerError: Error {
case noRefreshToken
case failedRefresh(Error)
case touchFailed(Error)
case revokeFailed(Error)
}
70 changes: 70 additions & 0 deletions Auth0Tests/CredentialsManagerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 97d19cf

Please sign in to comment.