Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DocC article explaining the most common renewal errors #824

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Auth0/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,9 @@ public protocol Authentication: Trackable, Loggable {

## See Also

- [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
- [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
- <doc:RefreshTokens>
*/
func renew(withRefreshToken refreshToken: String, scope: String?) -> Request<Credentials, AuthenticationError>

Expand Down
7 changes: 7 additions & 0 deletions Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import LocalAuthentication
/// ## See Also
///
/// - ``CredentialsManagerError``
/// - <doc:RefreshTokens>
public struct CredentialsManager {

private let storage: CredentialsStorage
Expand Down Expand Up @@ -280,6 +281,7 @@ public struct CredentialsManager {
///
/// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
/// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
/// - <doc:RefreshTokens>
public func credentials(withScope scope: String? = nil,
minTTL: Int = 0,
parameters: [String: Any] = [:],
Expand Down Expand Up @@ -351,6 +353,7 @@ public struct CredentialsManager {
///
/// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
/// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
/// - <doc:RefreshTokens>
public func renew(parameters: [String: Any] = [:],
headers: [String: String] = [:],
callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
Expand Down Expand Up @@ -568,6 +571,7 @@ public extension CredentialsManager {
///
/// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
/// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
/// - <doc:RefreshTokens>
func credentials(withScope scope: String? = nil,
minTTL: Int = 0,
parameters: [String: Any] = [:],
Expand Down Expand Up @@ -627,6 +631,7 @@ public extension CredentialsManager {
///
/// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
/// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
/// - <doc:RefreshTokens>
func renew(parameters: [String: Any] = [:],
headers: [String: String] = [:]) -> AnyPublisher<Credentials, CredentialsManagerError> {
return Deferred {
Expand Down Expand Up @@ -734,6 +739,7 @@ public extension CredentialsManager {
///
/// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
/// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
/// - <doc:RefreshTokens>
func credentials(withScope scope: String? = nil,
minTTL: Int = 0,
parameters: [String: Any] = [:],
Expand Down Expand Up @@ -783,6 +789,7 @@ public extension CredentialsManager {
///
/// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
/// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
/// - <doc:RefreshTokens>
func renew(parameters: [String: Any] = [:], headers: [String: String] = [:]) async throws -> Credentials {
return try await withCheckedThrowingContinuation { continuation in
self.renew(parameters: parameters, headers: headers, callback: continuation.resume)
Expand Down
70 changes: 70 additions & 0 deletions Documentation.docc/RefreshTokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Refresh Tokens: Common Errors

## Overview

Exchanging a refresh token for new credentials can produce the following errors, depending on the implementation of your app, and the configuration of your Auth0 application.

## Unknown or invalid refresh token

- **Example error description:** `The credentials renewal failed. CAUSE: Unknown or invalid refresh token`
- **Log event type:** `fertft`

This error means that Auth0 does not recognize the refresh token used to make the renewal request. Either an empty or garbage/truncated/padded token was sent, or the token is not valid anymore –it expired or was revoked.

### Common causes

#### Renewing the credentials after revoking the refresh token

If you revoke the refresh token –either through the Credentials Manager or the Authentication API client– you should not attempt to renew the credentials afterward. For example, by calling the `credentials()` method of the Credentials Manager in another thread.

Once a refresh token is revoked it can no longer be exchanged for new credentials.

#### Misconfiguration of the absolute lifetime of refresh tokens

The [absolute lifetime](https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-expiration) of the refresh token should not be shorter than the lifetime of the access token. If the access token is valid for longer than the refresh token is, your app may issue renewal requests with expired refresh tokens. This could happen, for example, when using the `credentials()` method of the Credentials Manager.

Once a refresh token expires it can no longer be exchanged for new credentials.

#### Misconfiguration of the inactivity lifetime of refresh tokens

The [inactivity lifetime](https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-expiration) of the refresh token should not be shorter than the lifetime of the access token. If the access token is valid for longer than the refresh token is *while the user is inactive*, your app may issue renewal requests with expired refresh tokens. This could happen, for example, when using the `credentials()` method of the Credentials Manager while your app is backgrounded.

Once a refresh token expires due to user inactivity it can no longer be exchanged for new credentials.

## Unsuccessful Refresh Token exchange, reused refresh token detected

- **Example error description:** `The credentials renewal failed. CAUSE: Unsuccessful Refresh Token exchange, reused refresh token detected`
- **Log event type:** `ferrt`

You might encounter this error if you have [Refresh Token Rotation](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) enabled for your Auth0 application. It means that the refresh token used to make the renewal request has already been exchanged, and the [reuse interval](https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-rotation) has passed.

Rotating refresh tokens are effectively single-use. The renewed credentials will come with a new refresh token, and the old one will be invalidated once the renew interval passes.

**In the absence of a bad actor, this error is typically a symptom of concurrency issues in your app.**

### Common causes

#### Multiple Credentials Manager instances

If you are using the Credentials Manager to renew the credentials -either through the `credentials()` or the `renew()` method- you should use a single Credentials Manager instance. While these methods are thread-safe, the Credentials Manager cannot synchronize them across instances.

This can happen, for example, by using a computed property to get a Credentials Manager instance:

```
struct Services {
// ❌ This will return a new instance every time
var credentialsManager: CredentialsManager {
return CredentialsManager(authentication: Auth0.authentication())
}
}
```

#### Using the Authentication API client to renew the credentials alongside the Credentials Manager

If you are using the Credentials Manager to store and retrieve the credentials, you should not force renewals through the `renew()` method of the Authentication API client. This method is not thread-safe, and may end up issuing renewal requests concurrently with the `credentials()` method of the Credentials Manager.

To force a renewal, use the ``CredentialsManager/renew(parameters:headers:callback:)`` method of the Credentials Manager instead. This method is thread-safe; the Credentials Manager will make sure only one renewal request is in flight at any given time.

#### Using the Authentication API client to renew the credentials without synchronization

If you are *not* using the Credentials Manager, you need to implement proper synchronization to call the `renew()` method of the Authentication API client. This method is not thread-safe; unless care is taken, concurrent renewal requests can happen.
6 changes: 3 additions & 3 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ You can make users land directly on the Signup page instead of the Login page by

| Parameters | No existing session | Existing session |
|:-----------------------------------------------|:----------------------|:------------------------------|
| No extra parameters | Shows the login page | Redirects to the callback url |
| `"screen_hint": "signup"` | Shows the signup page | Redirects to the callback url |
| No extra parameters | Shows the login page | Redirects to the callback URL |
| `"screen_hint": "signup"` | Shows the signup page | Redirects to the callback URL |
| `"prompt": "login"` | Shows the login page | Shows the login page |
| `"prompt": "login", "screen_hint": "signup"` | Shows the signup page | Shows the signup page |

Expand All @@ -44,7 +44,7 @@ Auth0
```

> [!NOTE]
> The `screen_hint` parameter will work with the **New Universal Login Experience** without any further configuration. If you are using the **Classic Universal Login Experience**, you need to customize the [login template](https://manage.auth0.com/#/login_page) to look for this parameter and set the `initialScreen` [option](https://github.com/auth0/lock#database-options) of the `Auth0Lock` constructor.
> The `screen_hint` parameter will work with the **New Universal Login Experience** without any further configuration. If you are using the **Classic Universal Login Experience**, you need to customize the [login template](https://manage.auth0.com/#/login_page) to look for this parameter and set the `initialScreen` [option](https://github.com/auth0/lock/blob/master/EXAMPLES.md#database-options) of the `Auth0Lock` constructor.

<details>
<summary>Using async/await</summary>
Expand Down