diff --git a/CLAUDE.md b/CLAUDE.md
index d0e40c3f..4937e70e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -105,14 +105,14 @@ react-native-auth0/
- Edit generated output: `lib/`, `docs/`, `coverage/`, `node_modules/`, `ios/build/`, `android/build/`, `example/**/build/`, Pods.
- Hand-edit `yarn.lock`.
- Remove or skip failing tests without fixing the underlying cause.
-- Disable PKCE or DPoP (both on by default).
+- Disable PKCE (on by default), or change the DPoP default without approval.
---
## Security Considerations
- **PKCE:** enabled by default — never disable.
-- **DPoP:** enabled by default (since v5.1.0).
+- **DPoP:** opt-in via `useDPoP: true` (was on by default in v5.1.0–v5.x; defaults to `false` from v6).
- **Secure storage:** iOS Keychain (SimpleKeychain) / Android EncryptedSharedPreferences; optional biometric protection with device-credential fallback.
- **Token handling:** never log tokens; treat access/refresh/ID tokens as sensitive throughout.
- **Static analysis:** Snyk (`.snyk`) and Semgrep (`.semgrepignore`) run in CI via `sca_scan.yml` — don't add ignore entries to suppress findings without approval.
diff --git a/EXAMPLES-WEB.md b/EXAMPLES-WEB.md
index 92308a3b..15f4d999 100644
--- a/EXAMPLES-WEB.md
+++ b/EXAMPLES-WEB.md
@@ -344,4 +344,4 @@ Token refresh is handled automatically by `credentialsManager.getCredentials()`
The [My Account API](./EXAMPLES.md#my-account-api) is supported on the web platform. The `auth0.myAccount` client works the same way as on native, so the examples in [EXAMPLES.md](./EXAMPLES.md#my-account-api) apply. Passkey enrollment on the web uses the browser's [WebAuthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) APIs instead of a native passkey module.
-When DPoP is enabled (the default), a My Account call on the web only succeeds when the supplied access token was issued by the same client instance, because the DPoP proof is signed with that client's keypair. When the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.
+When DPoP is enabled, a My Account call on the web only succeeds when the supplied access token was issued by the same client instance, because the DPoP proof is signed with that client's keypair. When the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.
diff --git a/EXAMPLES.md b/EXAMPLES.md
index cc8e4c2c..e696cdbb 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -1677,7 +1677,7 @@ The My Account API allows authenticated users to manage their own authentication
Access the My Account client via the `myAccount` property from `useAuth0()` or the `Auth0` class instance.
-The My Account API is supported on Native (iOS/Android) and Web. The same `myAccount` API is used on all platforms; only the passkey credential ceremony differs (native passkey module vs. the browser's WebAuthn APIs). On Web, when DPoP is enabled (the default), the supplied access token must have been issued by the same client instance, since the DPoP proof is signed with that client's keypair; when the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.
+The My Account API is supported on Native (iOS/Android) and Web. The same `myAccount` API is used on all platforms; only the passkey credential ceremony differs (native passkey module vs. the browser's WebAuthn APIs). On Web, when DPoP is enabled, the supplied access token must have been issued by the same client instance, since the DPoP proof is signed with that client's keypair; when the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.
### Prerequisites
@@ -2853,22 +2853,15 @@ if (credentials) {
### Enabling DPoP
-DPoP is enabled by default (`useDPoP: true`) when you initialize the Auth0 client:
+DPoP is opt-in (`useDPoP` defaults to `false`). Set `useDPoP: true` when you initialize the Auth0 client, and make sure DPoP is enabled for your application in the Auth0 Dashboard:
```js
import Auth0 from 'react-native-auth0';
-// DPoP is enabled by default
const auth0 = new Auth0({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
-});
-
-// Or explicitly enable it
-const auth0 = new Auth0({
- domain: 'YOUR_AUTH0_DOMAIN',
- clientId: 'YOUR_AUTH0_CLIENT_ID',
- useDPoP: true, // Explicitly enable DPoP
+ useDPoP: true,
});
```
@@ -2882,7 +2875,7 @@ function App() {
{/* Your app components */}
@@ -2892,6 +2885,8 @@ function App() {
> **Important**: DPoP will only be used for **new user sessions** created after enabling it. Existing sessions with Bearer tokens will continue to work until the user logs in again. See [Handling DPoP token migration](#handling-dpop-token-migration) for how to handle this transition.
+> **Turning DPoP off again**: if you previously ran with DPoP enabled, stored credentials are DPoP-bound. Reading them back with `useDPoP` unset (or `false`) fails with `DPOP_NOT_CONFIGURED`, because the credentials manager is no longer configured to prove possession of the key. Clear the stored credentials and have the user log in again when you turn DPoP off.
+
### Making API calls with DPoP
When calling your own APIs with DPoP-bound tokens, you need to include both the `Authorization` header and the `DPoP` proof header. The SDK provides a `getDPoPHeaders()` method to generate these headers:
diff --git a/FAQ.md b/FAQ.md
index 717a4ab3..4df1cb40 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -495,20 +495,20 @@ const credentials = await auth0.credentialsManager.getCredentials(
**Should you enable it?**
-DPoP is **enabled by default** (`useDPoP: true`) in this SDK because it provides significant security benefits with minimal impact on the developer experience. However, you should consider:
+DPoP is **opt-in** (`useDPoP` defaults to `false`), since it also has to be enabled for your application in the Auth0 Dashboard. Consider turning it on if:
-- ✅ Enable if you handle sensitive data or financial transactions
-- ✅ Enable if you want best-in-class security practices
-- ✅ Enable if your users access the app from multiple devices (DPoP helps prevent cross-device token abuse)
-- ⚠️ **Note**: Existing users with Bearer tokens will need to log in again to get DPoP tokens (see [FAQ #13](#13-how-do-i-migrate-existing-users-to-dpop))
+- ✅ You handle sensitive data or financial transactions
+- ✅ You want best-in-class security practices
+- ✅ Your users access the app from multiple devices (DPoP helps prevent cross-device token abuse)
+- ⚠️ **Note**: Existing users with Bearer tokens will need to log in again to get DPoP tokens (see [FAQ #14](#14-how-do-i-migrate-existing-users-to-dpop))
-**How to disable it (if needed):**
+**How to enable it:**
```javascript
const auth0 = new Auth0({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
- useDPoP: false, // Disable DPoP
+ useDPoP: true, // Enable DPoP
});
```
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
index d18cfe67..28bb8699 100644
--- a/MIGRATION_GUIDE.md
+++ b/MIGRATION_GUIDE.md
@@ -86,17 +86,46 @@ v6 adopts **Auth0.swift 3.0.1**, which is built with the **Swift 6** compiler.
2. Run `pod install --repo-update` in your `ios` directory to pick up Auth0.swift 3.0.1.
3. Use **Xcode 16** or later.
-> See §7 for the public-API changes that come with these native majors.
+> See §8 for the public-API changes that come with these native majors.
-### 5. Behavioral default shifts under native delegation ⏳
+### 5. DPoP is now opt-in ✅
+
+`useDPoP` now defaults to **`false`**. In v5.1.0–v5.x it defaulted to `true`, which meant every app got DPoP-bound tokens whether or not DPoP was enabled for the application in the Auth0 Dashboard. Since DPoP has to be turned on tenant-side to be useful, it is now something you opt into explicitly.
+
+**✅ Action Required:** if you rely on DPoP, set it explicitly:
+
+```diff
+ const auth0 = new Auth0({
+ domain: 'YOUR_AUTH0_DOMAIN',
+ clientId: 'YOUR_AUTH0_CLIENT_ID',
++ useDPoP: true,
+ });
+```
+
+If you never set `useDPoP` and don't need DPoP, no change is required — you will simply get Bearer tokens.
+
+> **Warning — existing sessions:** credentials saved by a DPoP-enabled v5 app are DPoP-bound. If you upgrade without setting `useDPoP: true`, the credentials manager is no longer configured to prove possession of the key, and reading those stored credentials fails with `DPOP_NOT_CONFIGURED` (`CredentialsManagerErrorCodes.DPOP_NOT_CONFIGURED`). Either set `useDPoP: true` to keep those sessions working, or clear the stored credentials and have the user log in again:
+>
+> ```js
+> try {
+> const credentials = await auth0.credentialsManager.getCredentials();
+> } catch (e) {
+> if (e.type === 'DPOP_NOT_CONFIGURED') {
+> await auth0.credentialsManager.clearCredentials();
+> // Send the user through authorize() again.
+> }
+> }
+> ```
+
+### 6. Behavioral default shifts under native delegation ⏳
_Planned — lands with full native auth delegation._ Routing all authentication through the native SDKs changes some defaults (e.g. `scope` gains `offline_access`, `minTTL` defaults to `60`, default connection names). Each shift and the action required will be documented here when that workstream merges.
-### 6. Management API (`users()`) removal ⏳
+### 7. Management API (`users()`) removal ⏳
_Planned._ The client-side Management API wrapper (`auth0.users(...)`) is being removed in v6, mirroring both native SDKs. Migrate Management operations to a backend/BFF. Full guidance will be added here.
-### 7. Native SDK API alignment (Auth0.Android v4 / Auth0.swift v3) ✅
+### 8. Native SDK API alignment (Auth0.Android v4 / Auth0.swift v3) ✅
Adopting the new native SDK majors changes two parts of the public surface.
diff --git a/README.md b/README.md
index 844b39f1..6d628b08 100644
--- a/README.md
+++ b/README.md
@@ -947,7 +947,7 @@ This library provides a unified API across Native (iOS/Android) and Web platform
| `myAccount.confirmPhoneEnrollment()` (and other `confirm...`) | ✅ | ✅ | Confirms an enrollment challenge and returns the enrolled authentication method. |
| `myAccount.passkeyEnrollmentChallenge()` / `myAccount.enrollPasskey()` | ✅ | ✅ | Enrolls a passkey as an authentication method. On Web, the browser's WebAuthn APIs handle the credential ceremony. |
-> **Note on DPoP (Web):** When the client is configured with DPoP (the default), My Account calls on Web only succeed when the supplied access token was issued by the same client instance, because the DPoP proof is signed with that client's keypair. When the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.
+> **Note on DPoP (Web):** When the client is configured with DPoP, My Account calls on Web only succeed when the supplied access token was issued by the same client instance, because the DPoP proof is signed with that client's keypair. When the client is not configured with DPoP, plain bearer tokens are used and any valid access token works.
## Troubleshooting
diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
index a6348ca6..fb9993d3 100644
--- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt
+++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
@@ -108,8 +108,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
CredentialsManagerException.SESSION_EXPIRED to "SESSION_EXPIRED",
CredentialsManagerException.SSO_EXCHANGE_FAILED to "SSO_EXCHANGE_FAILED"
)
- // DPoP enabled by default
- private var useDPoP: Boolean = true
+ // DPoP is opt-in
+ private var useDPoP: Boolean = false
private var auth0: Auth0? = null
private var mfaClient: MfaClient? = null
@@ -285,7 +285,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
// does not currently support retry configuration for credential renewal.
// This parameter is accepted for API compatibility with iOS.
- this.useDPoP = useDPoP ?: true
+ this.useDPoP = useDPoP ?: false
auth0 = Auth0.getInstance(clientId, domain)
mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext)
myAccount = MyAccount(auth0!!, this.useDPoP, reactContext)
diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts
index a826de02..7ef2e42b 100644
--- a/src/platforms/native/adapters/NativeAuth0Client.ts
+++ b/src/platforms/native/adapters/NativeAuth0Client.ts
@@ -64,7 +64,7 @@ export class NativeAuth0Client implements IAuth0Client {
this.configSignature = getConfigSignature(options);
const baseUrl = `https://${options.domain}`;
this.baseUrl = baseUrl;
- const useDPoP = options.useDPoP ?? true;
+ const useDPoP = options.useDPoP ?? false;
this.tokenType = useDPoP ? TokenType.dpop : TokenType.bearer;
this.httpClient = new HttpClient({
@@ -116,7 +116,7 @@ export class NativeAuth0Client implements IAuth0Client {
clientId,
domain,
localAuthenticationOptions,
- useDPoP = true,
+ useDPoP = false,
maxRetries,
credentialsManagerStorageKey,
} = options;
diff --git a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
index 914a65d9..8b0c97de 100644
--- a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
+++ b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
@@ -118,7 +118,7 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
undefined, // No local auth options provided in this test
- true, // useDPoP defaults to true
+ false, // useDPoP defaults to false
undefined, // maxRetries not provided
undefined // credentialsManagerStorageKey not provided
);
@@ -140,7 +140,7 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
undefined,
- true,
+ false,
undefined,
'tenant-b'
);
@@ -161,7 +161,7 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
localAuthOptions,
- true, // useDPoP defaults to true
+ false, // useDPoP defaults to false
undefined, // maxRetries not provided
undefined // credentialsManagerStorageKey not provided
);
@@ -684,7 +684,7 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
undefined,
- true,
+ false,
undefined,
undefined
);
diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts
index fe7936ce..b99c7886 100644
--- a/src/platforms/native/bridge/NativeBridgeManager.ts
+++ b/src/platforms/native/bridge/NativeBridgeManager.ts
@@ -58,7 +58,7 @@ export class NativeBridgeManager implements INativeBridge {
clientId: string,
domain: string,
localAuthenticationOptions?: LocalAuthenticationOptions,
- useDPoP: boolean = true,
+ useDPoP: boolean = false,
maxRetries: number = 0,
credentialsManagerStorageKey?: string
): Promise {
diff --git a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts
index 6151ac8f..15e7c48f 100644
--- a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts
+++ b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts
@@ -250,7 +250,7 @@ describe('NativeBridgeManager', () => {
'client-id',
'tenant-a.auth0.com',
undefined, // localAuthenticationOptions
- true, // useDPoP default
+ false, // useDPoP default
0, // maxRetries default
undefined // credentialsManagerStorageKey
);
diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts
index 3d170090..614173c9 100644
--- a/src/platforms/web/adapters/WebAuth0Client.ts
+++ b/src/platforms/web/adapters/WebAuth0Client.ts
@@ -83,7 +83,7 @@ export class WebAuth0Client implements IAuth0Client {
constructor(options: WebAuth0Options) {
const baseUrl = `https://${options.domain}`;
this.baseUrl = baseUrl;
- const useDPoP = options.useDPoP ?? true;
+ const useDPoP = options.useDPoP ?? false;
this.tokenType = useDPoP ? TokenType.dpop : TokenType.bearer;
this.httpClient = new HttpClient({
@@ -100,7 +100,7 @@ export class WebAuth0Client implements IAuth0Client {
// MRRT requires refresh tokens to work - automatically enable if useMrrt is true
useRefreshTokens: options.useRefreshTokens ?? options.useMrrt ?? false,
useRefreshTokensFallback: options.useRefreshTokensFallback ?? true,
- useDpop: options.useDPoP ?? true,
+ useDpop: useDPoP,
authorizationParams: {
redirect_uri:
typeof window !== 'undefined' ? window.location.origin : '',
diff --git a/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts b/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts
index ca5f981e..a66c531f 100644
--- a/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts
+++ b/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts
@@ -192,7 +192,7 @@ describe('WebAuth0Client', () => {
expect.objectContaining({
clientId: defaultOptions.clientId,
httpClient: mockHttpClient,
- tokenType: 'DPoP',
+ tokenType: 'Bearer',
baseUrl: `https://${defaultOptions.domain}`,
})
);
@@ -201,6 +201,23 @@ describe('WebAuth0Client', () => {
expect(MockWebCredentialsManager).toHaveBeenCalledWith(mockSpaClient);
});
+ it('should use DPoP token type when useDPoP is enabled', () => {
+ MockAuthenticationOrchestrator.mockClear();
+
+ const dpopClient = new WebAuth0Client({
+ ...defaultOptions,
+ useDPoP: true,
+ });
+
+ expect(dpopClient).toBeDefined();
+ expect(MockAuthenticationOrchestrator).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tokenType: 'DPoP',
+ getDPoPHeaders: expect.any(Function),
+ })
+ );
+ });
+
it('should initialize with custom options', () => {
const customOptions = {
domain: 'custom.auth0.com',
@@ -255,9 +272,17 @@ describe('WebAuth0Client', () => {
});
describe('users method', () => {
+ // DPoP is opt-in, so build a DPoP-enabled client to keep covering that path.
+ let dpopClient: WebAuth0Client;
+
+ beforeEach(() => {
+ dpopClient = new WebAuth0Client({ ...defaultOptions, useDPoP: true });
+ MockManagementApiOrchestrator.mockClear();
+ });
+
it('should create and return ManagementApiOrchestrator instance', () => {
const token = 'access_token_123';
- const usersClient = client.users(token);
+ const usersClient = dpopClient.users(token);
expect(MockManagementApiOrchestrator).toHaveBeenCalledWith({
token,
@@ -269,14 +294,24 @@ describe('WebAuth0Client', () => {
expect(usersClient).toBeDefined();
});
- it('should create new instance for each call', () => {
- MockManagementApiOrchestrator.mockClear();
+ it('should default to Bearer when DPoP is not enabled', () => {
+ client.users('access_token_123');
+ expect(MockManagementApiOrchestrator).toHaveBeenCalledWith({
+ token: 'access_token_123',
+ httpClient: mockHttpClient,
+ tokenType: 'Bearer',
+ baseUrl: `https://${defaultOptions.domain}`,
+ getDPoPHeaders: undefined,
+ });
+ });
+
+ it('should create new instance for each call', () => {
const token1 = 'token1';
const token2 = 'token2';
- client.users(token1);
- client.users(token2);
+ dpopClient.users(token1);
+ dpopClient.users(token2);
expect(MockManagementApiOrchestrator).toHaveBeenCalledTimes(2);
expect(MockManagementApiOrchestrator).toHaveBeenNthCalledWith(1, {
@@ -470,17 +505,21 @@ describe('WebAuth0Client', () => {
});
it('should use client tokenType as fallback when response token_type is missing', async () => {
+ const dpopClient = new WebAuth0Client({
+ ...defaultOptions,
+ useDPoP: true,
+ });
mockSpaClient.loginWithCustomTokenExchange.mockResolvedValueOnce({
...mockExchangeResponse,
token_type: undefined,
});
- const result = await client.customTokenExchange({
+ const result = await dpopClient.customTokenExchange({
subjectToken: 'external-token',
subjectTokenType: 'urn:acme:legacy-token',
});
- // Should use client's default tokenType (DPoP)
+ // Should use client's configured tokenType (DPoP)
expect(result.tokenType).toBe('DPoP');
});
diff --git a/src/types/common.ts b/src/types/common.ts
index 912581ba..aad81d15 100644
--- a/src/types/common.ts
+++ b/src/types/common.ts
@@ -191,7 +191,8 @@ export interface Auth0Options {
/**
* Enables DPoP (Demonstrating Proof-of-Possession) for enhanced token security.
* When enabled, access and refresh tokens are cryptographically bound to a client-specific key pair.
- * @default true
+ * Requires DPoP to be enabled for your application in the Auth0 Dashboard.
+ * @default false
* @see https://datatracker.ietf.org/doc/html/rfc9449
*/
useDPoP?: boolean;