From acd76ff6314eaabff7b6f9be235acc846b93f1a4 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Tue, 14 Jul 2026 00:06:13 +0530 Subject: [PATCH 1/3] feat: expose clearDPoPKey() on class and hook APIs --- FAQ.md | 53 +++++++++++++++++++ src/Auth0.ts | 20 +++++++ src/core/interfaces/IAuth0Client.ts | 13 +++++ src/hooks/Auth0Context.ts | 14 +++++ src/hooks/Auth0Provider.tsx | 12 +++++ .../native/adapters/NativeAuth0Client.ts | 12 +++++ src/platforms/web/adapters/WebAuth0Client.ts | 10 ++++ 7 files changed, 134 insertions(+) diff --git a/FAQ.md b/FAQ.md index 717a4ab36..9c2a6aed3 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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? @@ -1038,3 +1039,55 @@ 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()` + +Use `clearDPoPKey()` to clear the DPoP key pair and cached state, then retry the login. This gives the next login a clean key pair and a fresh handshake. + +```javascript +import { useAuth0 } from 'react-native-auth0'; + +function LoginScreen() { + const { authorize, clearDPoPKey } = useAuth0(); + + const handleLogin = async () => { + try { + await authorize({ scope: 'openid profile email offline_access' }); + } catch (error) { + // If a login fails and you suspect corrupted DPoP state, + // clear the key and retry once. + await clearDPoPKey(); + await authorize({ scope: 'openid profile email offline_access' }); + } + }; + + // ... +} +``` + +Using the class-based API: + +```javascript +try { + await auth0.webAuth.authorize(); +} catch (error) { + 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. + +### Last resort: disable DPoP + +If you cannot recover through `clearDPoPKey()`, you can disable DPoP entirely with `useDPoP: false`. This is a **security downgrade** and should be a last resort — see [FAQ #16](#16-what-happens-if-i-disable-dpop-after-enabling-it). diff --git a/src/Auth0.ts b/src/Auth0.ts index fbd700d40..0630896d0 100644 --- a/src/Auth0.ts +++ b/src/Auth0.ts @@ -135,6 +135,26 @@ 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). + * + * @returns A promise that resolves when the key has been cleared. + * + * @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. diff --git a/src/core/interfaces/IAuth0Client.ts b/src/core/interfaces/IAuth0Client.ts index ce091dd49..12ee0dba0 100644 --- a/src/core/interfaces/IAuth0Client.ts +++ b/src/core/interfaces/IAuth0Client.ts @@ -82,6 +82,19 @@ export interface IAuth0Client { */ getDPoPHeaders(params: DPoPHeadersParams): Promise>; + /** + * 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). + * + * @returns A promise that resolves when the key has been cleared. + */ + clearDPoPKey(): Promise; + /** * Performs a Custom Token Exchange using RFC 8693. * Exchanges an external identity provider token for Auth0 tokens. diff --git a/src/hooks/Auth0Context.ts b/src/hooks/Auth0Context.ts index 2992aa24a..103e58d5a 100644 --- a/src/hooks/Auth0Context.ts +++ b/src/hooks/Auth0Context.ts @@ -421,6 +421,19 @@ export interface Auth0ContextInterface extends AuthState { params: DPoPHeadersParams ) => Promise>; + /** + * 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). + * + * @returns A promise that resolves when the key has been cleared. + */ + clearDPoPKey: () => Promise; + /** * Obtains session transfer credentials for performing Native to Web SSO. * @@ -548,6 +561,7 @@ const initialContext: Auth0ContextInterface = { resetPassword: stub, revokeRefreshToken: stub, getDPoPHeaders: stub, + clearDPoPKey: stub, getSSOCredentials: stub, ssoExchange: stub, }; diff --git a/src/hooks/Auth0Provider.tsx b/src/hooks/Auth0Provider.tsx index 3441c5a3e..ce8282a55 100644 --- a/src/hooks/Auth0Provider.tsx +++ b/src/hooks/Auth0Provider.tsx @@ -477,6 +477,16 @@ export const Auth0Provider = ({ [client] ); + const clearDPoPKey = useCallback(async (): Promise => { + 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 @@ -527,6 +537,7 @@ export const Auth0Provider = ({ authorizeWithRecoveryCode, revokeRefreshToken, getDPoPHeaders, + clearDPoPKey, ssoExchange, }), [ @@ -563,6 +574,7 @@ export const Auth0Provider = ({ authorizeWithRecoveryCode, revokeRefreshToken, getDPoPHeaders, + clearDPoPKey, ssoExchange, ] ); diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index e81d7bf44..8f9da6015 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -180,6 +180,18 @@ export class NativeAuth0Client implements IAuth0Client { } } + async clearDPoPKey(): Promise { + await this.ready; + try { + return await this.bridge.clearDPoPKey(); + } catch (e) { + if (e instanceof AuthError) { + throw new DPoPError(e); + } + throw e; + } + } + private createGuardedBridge(bridge: INativeBridge): INativeBridge { const guarded: any = {}; diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index d0c41d9c9..9653ed5f4 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -225,6 +225,16 @@ export class WebAuth0Client implements IAuth0Client { } } + async clearDPoPKey(): Promise { + // 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 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. From ff18408a10fe1b84817fdf04a22a66ad5aef1686 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Tue, 14 Jul 2026 00:18:31 +0530 Subject: [PATCH 2/3] feat: enhance clearDPoPKey() documentation and clarify usage across platforms --- FAQ.md | 28 ++++++++----------- src/Auth0.ts | 5 ++++ src/core/interfaces/IAuth0Client.ts | 5 ++++ src/hooks/Auth0Context.ts | 5 ++++ .../native/adapters/NativeAuth0Client.ts | 10 +++++++ src/platforms/web/adapters/WebAuth0Client.ts | 10 +++++++ 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/FAQ.md b/FAQ.md index 9c2a6aed3..5ebb440a9 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1048,7 +1048,9 @@ A common trigger is an **interrupted authentication flow** — for example, the ### The solution: `clearDPoPKey()` -Use `clearDPoPKey()` to clear the DPoP key pair and cached state, then retry the login. This gives the next login a clean key pair and a fresh handshake. +`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'; @@ -1056,15 +1058,11 @@ import { useAuth0 } from 'react-native-auth0'; function LoginScreen() { const { authorize, clearDPoPKey } = useAuth0(); - const handleLogin = async () => { - try { - await authorize({ scope: 'openid profile email offline_access' }); - } catch (error) { - // If a login fails and you suspect corrupted DPoP state, - // clear the key and retry once. - await clearDPoPKey(); - await authorize({ scope: 'openid profile email offline_access' }); - } + // 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' }); }; // ... @@ -1074,9 +1072,9 @@ function LoginScreen() { Using the class-based API: ```javascript -try { - await auth0.webAuth.authorize(); -} catch (error) { +// 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(); } @@ -1087,7 +1085,3 @@ try { ### 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. - -### Last resort: disable DPoP - -If you cannot recover through `clearDPoPKey()`, you can disable DPoP entirely with `useDPoP: false`. This is a **security downgrade** and should be a last resort — see [FAQ #16](#16-what-happens-if-i-disable-dpop-after-enabling-it). diff --git a/src/Auth0.ts b/src/Auth0.ts index 0630896d0..689d3f779 100644 --- a/src/Auth0.ts +++ b/src/Auth0.ts @@ -144,7 +144,12 @@ class Auth0 { * 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 An `AuthError` with code `UnsupportedOperation` on the web platform. * * @example * ```typescript diff --git a/src/core/interfaces/IAuth0Client.ts b/src/core/interfaces/IAuth0Client.ts index 12ee0dba0..16a727c39 100644 --- a/src/core/interfaces/IAuth0Client.ts +++ b/src/core/interfaces/IAuth0Client.ts @@ -91,7 +91,12 @@ export interface IAuth0Client { * 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 An `AuthError` with code `UnsupportedOperation` on the web platform. */ clearDPoPKey(): Promise; diff --git a/src/hooks/Auth0Context.ts b/src/hooks/Auth0Context.ts index 103e58d5a..2fbfd7575 100644 --- a/src/hooks/Auth0Context.ts +++ b/src/hooks/Auth0Context.ts @@ -430,7 +430,12 @@ export interface Auth0ContextInterface extends AuthState { * 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 An `AuthError` with code `UnsupportedOperation` on the web platform. */ clearDPoPKey: () => Promise; diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index 8f9da6015..715461f55 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -180,6 +180,16 @@ 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 { await this.ready; try { diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index 9653ed5f4..2898a1478 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -225,6 +225,16 @@ 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 An `AuthError` with code `UnsupportedOperation`. + */ async clearDPoPKey(): Promise { // 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 From f5746e553e7a05dc62945d1ba056b0fc5d32489f Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Tue, 14 Jul 2026 00:34:06 +0530 Subject: [PATCH 3/3] feat: update error handling for clearDPoPKey() to use DPoPError across platforms --- src/Auth0.ts | 2 +- src/core/interfaces/IAuth0Client.ts | 2 +- src/hooks/Auth0Context.ts | 2 +- src/platforms/native/adapters/NativeAuth0Client.ts | 2 +- src/platforms/web/adapters/WebAuth0Client.ts | 10 ++++++---- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Auth0.ts b/src/Auth0.ts index 689d3f779..39a37c2f6 100644 --- a/src/Auth0.ts +++ b/src/Auth0.ts @@ -149,7 +149,7 @@ class Auth0 { * and cleared automatically on logout. * * @returns A promise that resolves when the key has been cleared. - * @throws An `AuthError` with code `UnsupportedOperation` on the web platform. + * @throws A `DPoPError` (wrapping an `AuthError` with code `UnsupportedOperation`) on the web platform. * * @example * ```typescript diff --git a/src/core/interfaces/IAuth0Client.ts b/src/core/interfaces/IAuth0Client.ts index 16a727c39..2ce98257c 100644 --- a/src/core/interfaces/IAuth0Client.ts +++ b/src/core/interfaces/IAuth0Client.ts @@ -96,7 +96,7 @@ export interface IAuth0Client { * and cleared automatically on logout. * * @returns A promise that resolves when the key has been cleared. - * @throws An `AuthError` with code `UnsupportedOperation` on the web platform. + * @throws A `DPoPError` (wrapping an `AuthError` with code `UnsupportedOperation`) on the web platform. */ clearDPoPKey(): Promise; diff --git a/src/hooks/Auth0Context.ts b/src/hooks/Auth0Context.ts index 2fbfd7575..d0eadeca9 100644 --- a/src/hooks/Auth0Context.ts +++ b/src/hooks/Auth0Context.ts @@ -435,7 +435,7 @@ export interface Auth0ContextInterface extends AuthState { * and cleared automatically on logout. * * @returns A promise that resolves when the key has been cleared. - * @throws An `AuthError` with code `UnsupportedOperation` on the web platform. + * @throws A `DPoPError` (wrapping an `AuthError` with code `UnsupportedOperation`) on the web platform. */ clearDPoPKey: () => Promise; diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index 715461f55..3ee0a8fad 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -193,7 +193,7 @@ export class NativeAuth0Client implements IAuth0Client { async clearDPoPKey(): Promise { await this.ready; try { - return await this.bridge.clearDPoPKey(); + return await this.guardedBridge.clearDPoPKey(); } catch (e) { if (e instanceof AuthError) { throw new DPoPError(e); diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index 2898a1478..0fabee369 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -233,15 +233,17 @@ export class WebAuth0Client implements IAuth0Client { * internal DPoP store is cleared as part of logout. There is no supported * way to clear it independently on web. * - * @throws An `AuthError` with code `UnsupportedOperation`. + * @throws A `DPoPError` wrapping an `AuthError` with code `UnsupportedOperation`. */ async clearDPoPKey(): Promise { // 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 AuthError( - 'UnsupportedOperation', - 'clearDPoPKey is not supported on the web platform. The DPoP key is managed by auth0-spa-js and cleared on logout.' + 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.' + ) ); }