Skip to content
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion EXAMPLES-WEB.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
17 changes: 6 additions & 11 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
});
```

Expand All @@ -2882,7 +2875,7 @@ function App() {
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
// DPoP is enabled by default
useDPoP={true}
>
{/* Your app components */}
</Auth0Provider>
Expand All @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
```

Expand Down
37 changes: 33 additions & 4 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/platforms/native/adapters/NativeAuth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -116,7 +116,7 @@ export class NativeAuth0Client implements IAuth0Client {
clientId,
domain,
localAuthenticationOptions,
useDPoP = true,
useDPoP = false,
maxRetries,
credentialsManagerStorageKey,
} = options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand All @@ -140,7 +140,7 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
undefined,
true,
false,
undefined,
'tenant-b'
);
Expand All @@ -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
);
Expand Down Expand Up @@ -684,7 +684,7 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
undefined,
true,
false,
undefined,
undefined
);
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/native/bridge/NativeBridgeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
4 changes: 2 additions & 2 deletions src/platforms/web/adapters/WebAuth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 : '',
Expand Down
55 changes: 47 additions & 8 deletions src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe('WebAuth0Client', () => {
expect.objectContaining({
clientId: defaultOptions.clientId,
httpClient: mockHttpClient,
tokenType: 'DPoP',
tokenType: 'Bearer',
baseUrl: `https://${defaultOptions.domain}`,
})
);
Expand All @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -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, {
Expand Down Expand Up @@ -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');
});

Expand Down
3 changes: 2 additions & 1 deletion src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading