Skip to content
Open
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
47 changes: 47 additions & 0 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
16. [What happens if I disable DPoP after enabling it?](#16-what-happens-if-i-disable-dpop-after-enabling-it)
17. [Why does the app hang or freeze during Social Login (Google, Facebook, etc.)?](#17-why-does-the-app-hang-or-freeze-during-social-login-google-facebook-etc)
18. [How do I refresh the user profile (e.g. `emailVerified`) after it changes on the server?](#18-how-do-i-refresh-the-user-profile-eg-emailverified-after-it-changes-on-the-server)
19. [How do I recover from a corrupted DPoP state?](#19-how-do-i-recover-from-a-corrupted-dpop-state)

## 1. How can I have separate Auth0 domains for each environment on Android?

Expand Down Expand Up @@ -1038,3 +1039,49 @@ function VerifyEmailScreen() {
| `getCredentials` promise never resolves | Missing refresh token or network issue | Ensure `offline_access` is included during login, and check network connectivity. |

> **Note**: This behavior differs from the web SDK (`@auth0/auth0-spa-js`), where token refresh is handled automatically via silent authentication using iframes. On native platforms (iOS/Android), a refresh token is explicitly required.

## 19. How do I recover from a corrupted DPoP state?

When DPoP is enabled, the SDK generates a device-bound key pair and negotiates cryptographic state with Auth0 during the token exchange. In certain edge cases this state can become desynchronized, and subsequent logins fail even though credentials and configuration are correct.

A common trigger is an **interrupted authentication flow** — for example, the user deep-links back into the app while an authorization transaction is still in flight (such as returning from an email password reset), orphaning the original transaction and leaving stale DPoP state behind.

### The solution: `clearDPoPKey()`

`clearDPoPKey()` clears the DPoP key pair and cached state so the next login starts with a clean key pair and a fresh handshake.

Treat this as a **deliberate recovery action**, not automatic error handling. Do **not** wrap it around every failed `authorize()` — most login failures (user cancellation, invalid credentials, network errors) have nothing to do with DPoP, and clearing the key on those needlessly invalidates a valid DPoP-bound session. Call it only when you have specifically diagnosed a corrupted DPoP state (for example, repeated login failures following an interrupted flow):

```javascript
import { useAuth0 } from 'react-native-auth0';

function LoginScreen() {
const { authorize, clearDPoPKey } = useAuth0();

// Expose recovery as an explicit user action, e.g. a "Reset and try again"
// button shown after the user has hit repeated, unexplained login failures.
const recoverAndLogin = async () => {
await clearDPoPKey();
await authorize({ scope: 'openid profile email offline_access' });
};

// ...
}
```

Using the class-based API:

```javascript
// Called deliberately when a corrupted DPoP state has been diagnosed,
// not as a blanket catch handler around authorize().
async function recoverAndLogin() {
await auth0.clearDPoPKey();
await auth0.webAuth.authorize();
}
```

> **Warning**: DPoP-bound access and refresh tokens are cryptographically tied to the key pair. Calling `clearDPoPKey()` **invalidates any existing DPoP-bound session** — the stored refresh token can no longer mint new tokens. Only call it when there is no valid authenticated session to preserve (such as before a fresh login, or as part of logout), not while an authenticated user is active. If you call it on an active session, the user will need to log in again.

### Platform support

`clearDPoPKey()` is supported on **iOS and Android**. On the **web** platform it throws an `UnsupportedOperation` error, because `@auth0/auth0-spa-js` manages the DPoP key internally and clears it as part of `logout()`; there is no supported way to clear it independently.
25 changes: 25 additions & 0 deletions src/Auth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,31 @@ class Auth0 {
return this.client.getDPoPHeaders(params);
}

/**
* Clears the DPoP key pair and cached nonce from secure storage.
*
* @remarks
* DPoP-bound tokens are cryptographically tied to this key pair, so clearing it
* invalidates any existing DPoP-bound session. Call this on logout, or to recover
* from a corrupted DPoP transaction state (for example, after an interrupted login
* flow leaves stale cryptographic state that breaks subsequent logins).
*
* Supported on iOS and Android. On the web platform this throws an
* `UnsupportedOperation` error, since the DPoP key is managed by auth0-spa-js
* and cleared automatically on logout.
*
* @returns A promise that resolves when the key has been cleared.
* @throws A `DPoPError` (wrapping an `AuthError` with code `UnsupportedOperation`) on the web platform.
*
* @example
* ```typescript
* await auth0.clearDPoPKey();
* ```
*/
clearDPoPKey() {
return this.client.clearDPoPKey();
}

/**
* Performs a Custom Token Exchange using RFC 8693.
* Exchanges an external identity provider token for Auth0 tokens.
Expand Down
18 changes: 18 additions & 0 deletions src/core/interfaces/IAuth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ export interface IAuth0Client {
*/
getDPoPHeaders(params: DPoPHeadersParams): Promise<Record<string, string>>;

/**
* Clears the DPoP key pair and cached nonce from secure storage.
*
* @remarks
* DPoP-bound tokens are cryptographically tied to this key pair, so clearing it
* invalidates any existing DPoP-bound session. Call this on logout, or to recover
* from a corrupted DPoP transaction state (for example, after an interrupted login
* flow leaves stale cryptographic state that breaks subsequent logins).
*
* Supported on iOS and Android. On the web platform this throws an
* `UnsupportedOperation` error, since the DPoP key is managed by auth0-spa-js
* and cleared automatically on logout.
*
* @returns A promise that resolves when the key has been cleared.
* @throws A `DPoPError` (wrapping an `AuthError` with code `UnsupportedOperation`) on the web platform.
*/
clearDPoPKey(): Promise<void>;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Performs a Custom Token Exchange using RFC 8693.
* Exchanges an external identity provider token for Auth0 tokens.
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/Auth0Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,24 @@ export interface Auth0ContextInterface extends AuthState {
params: DPoPHeadersParams
) => Promise<Record<string, string>>;

/**
* Clears the DPoP key pair and cached nonce from secure storage.
*
* @remarks
* DPoP-bound tokens are cryptographically tied to this key pair, so clearing it
* invalidates any existing DPoP-bound session. Call this on logout, or to recover
* from a corrupted DPoP transaction state (for example, after an interrupted login
* flow leaves stale cryptographic state that breaks subsequent logins).
*
* Supported on iOS and Android. On the web platform this throws an
* `UnsupportedOperation` error, since the DPoP key is managed by auth0-spa-js
* and cleared automatically on logout.
*
* @returns A promise that resolves when the key has been cleared.
* @throws A `DPoPError` (wrapping an `AuthError` with code `UnsupportedOperation`) on the web platform.
*/
clearDPoPKey: () => Promise<void>;

/**
* Obtains session transfer credentials for performing Native to Web SSO.
*
Expand Down Expand Up @@ -570,6 +588,7 @@ const initialContext: Auth0ContextInterface = {
resetPassword: stub,
revokeRefreshToken: stub,
getDPoPHeaders: stub,
clearDPoPKey: stub,
getSSOCredentials: stub,
ssoExchange: stub,
mfa: {
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/Auth0Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,16 @@ export const Auth0Provider = ({
[client]
);

const clearDPoPKey = useCallback(async (): Promise<void> => {
try {
return await client.clearDPoPKey();
} catch (e) {
const error = e as AuthError;
dispatch({ type: 'ERROR', error });
throw error;
}
}, [client]);

const ssoExchange = useCallback(
async (
parameters: SSOExchangeParameters
Expand Down Expand Up @@ -574,6 +584,7 @@ export const Auth0Provider = ({
authorizeWithRecoveryCode,
revokeRefreshToken,
getDPoPHeaders,
clearDPoPKey,
ssoExchange,
mfa,
}),
Expand Down Expand Up @@ -611,6 +622,7 @@ export const Auth0Provider = ({
authorizeWithRecoveryCode,
revokeRefreshToken,
getDPoPHeaders,
clearDPoPKey,
ssoExchange,
mfa,
]
Expand Down
22 changes: 22 additions & 0 deletions src/platforms/native/adapters/NativeAuth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,28 @@ export class NativeAuth0Client implements IAuth0Client {
}
}

/**
* Clears the DPoP key pair and cached nonce from secure storage.
*
* @remarks
* DPoP-bound tokens are cryptographically tied to this key pair, so clearing it
* invalidates any existing DPoP-bound session. Delegates to the native SDK,
* which is supported on both iOS and Android.
*
* @returns A promise that resolves when the key has been cleared.
*/
async clearDPoPKey(): Promise<void> {
await this.ready;
try {
return await this.guardedBridge.clearDPoPKey();
} catch (e) {
if (e instanceof AuthError) {
throw new DPoPError(e);
}
throw e;
}
}

private createGuardedBridge(bridge: INativeBridge): INativeBridge {
const guarded: any = {};

Expand Down
22 changes: 22 additions & 0 deletions src/platforms/web/adapters/WebAuth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,28 @@ export class WebAuth0Client implements IAuth0Client {
}
}

/**
* Not supported on the web platform.
*
* @remarks
* auth0-spa-js does not expose a public method to clear the DPoP key; its
* internal DPoP store is cleared as part of logout. There is no supported
* way to clear it independently on web.
*
* @throws A `DPoPError` wrapping an `AuthError` with code `UnsupportedOperation`.
*/
async clearDPoPKey(): Promise<void> {
// auth0-spa-js does not expose a public method to clear the DPoP key; its
// internal Dpop store is cleared as part of logout. There is no supported
// way to clear it independently on web.
throw new DPoPError(
new AuthError(
'UnsupportedOperation',
'clearDPoPKey is not supported on the web platform. The DPoP key is managed by auth0-spa-js and cleared on logout.'
)
);
}

/**
* Performs a Custom Token Exchange using RFC 8693.
* Exchanges an external identity provider token for Auth0 tokens.
Expand Down
Loading