Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
877f488
feat: mfa error bubbling support
tusharpandey13 Jan 7, 2026
b59e97f
feat: add mfa example app and update docs
tusharpandey13 Jan 9, 2026
248eca7
Merge remote-tracking branch 'origin/main' into feat/mfa-base
tusharpandey13 Jan 9, 2026
d89cfa4
chore: fix example app
tusharpandey13 Jan 9, 2026
deb5bdb
chore: fix example app
tusharpandey13 Jan 9, 2026
c572c4a
chore: remove irrelevant code
tusharpandey13 Jan 9, 2026
0dbaffa
fix: use createHmac intead of createHash
tusharpandey13 Jan 12, 2026
ec0d87b
docs: add readme for mfa example app
tusharpandey13 Jan 12, 2026
742724a
chore: lint
tusharpandey13 Jan 12, 2026
b2f92e5
chore: add AGENTS.md
tusharpandey13 Jan 13, 2026
7af2c3b
Merge branch 'chore/agent-support' into feat/mfa-apis
tusharpandey13 Jan 13, 2026
e276681
feat: simplify mfa base implementation; move to a stateless design
tusharpandey13 Jan 21, 2026
dfb2467
chore: fix tests and lint
tusharpandey13 Jan 21, 2026
cfef8dd
chore: remove irrelevant code
tusharpandey13 Jan 21, 2026
a02f859
Merge branch 'feat/mfa-base' of https://github.com/auth0/nextjs-auth0…
tusharpandey13 Jan 21, 2026
db04501
feat: add types for mfa apis; move MfaContext type to types/mfa.ts file
tusharpandey13 Jan 26, 2026
004c1dd
Merge branch 'main' of https://github.com/auth0/nextjs-auth0 into fea…
tusharpandey13 Jan 26, 2026
b4e1d0b
feat: add mfa api business logic + errors; update mfa types and const…
tusharpandey13 Jan 26, 2026
30ae3e6
chore: remove irrelevant code; add return type for mfaVerify
tusharpandey13 Jan 26, 2026
6864537
feat: add server mfa client; update mfa business logic methods in aut…
tusharpandey13 Jan 26, 2026
123b585
chore: lint
tusharpandey13 Jan 26, 2026
ac7a1cf
feat: add mfa_token re-encryption in chained mfa usecase; move sessio…
tusharpandey13 Jan 26, 2026
4b56933
chore: lint
tusharpandey13 Jan 26, 2026
7278067
feat: add final validation in mfaVerify
tusharpandey13 Jan 26, 2026
3ae876a
feat: add mfa handlers; add mfa error handler helper
tusharpandey13 Jan 26, 2026
aab62e9
feat: add mfa client helpers; refactor server mfa error handling
tusharpandey13 Jan 26, 2026
6e1a368
Merge branch 'main' of https://github.com/auth0/nextjs-auth0 into fea…
tusharpandey13 Jan 26, 2026
90cf945
Merge branch 'feat/mfa-base' of https://github.com/auth0/nextjs-auth0…
tusharpandey13 Jan 26, 2026
4a4de9b
feat: externalize internal mfa endpoints
tusharpandey13 Jan 27, 2026
4f35380
fix: correctly handle jwe decryption errors
tusharpandey13 Jan 27, 2026
44461a4
Merge branch 'feat/mfa-base' of https://github.com/auth0/nextjs-auth0…
tusharpandey13 Jan 27, 2026
ac7ec4d
chore: remove example app
tusharpandey13 Jan 27, 2026
e4e85d6
docs: fix docs
tusharpandey13 Jan 27, 2026
d633b3f
docs: fix docs
tusharpandey13 Jan 27, 2026
c4e021b
Merge branch 'feat/mfa-base' of https://github.com/auth0/nextjs-auth0…
tusharpandey13 Jan 27, 2026
eefed32
feat: add enroll and deleteAuthenticators; extract validation and tra…
tusharpandey13 Jan 27, 2026
ee31989
docs: fix mfa action in examples
tusharpandey13 Jan 28, 2026
299ad79
docs: fix mfa action in examples
tusharpandey13 Jan 28, 2026
a918539
fix: send correct audience in verify call; send clientId and clientSe…
tusharpandey13 Jan 28, 2026
b418b01
chore: update AGENTS.md
tusharpandey13 Jan 28, 2026
c8c7831
feat: add mfa example app
tusharpandey13 Jan 28, 2026
4b7fd41
chore: move example app to separate branch
tusharpandey13 Jan 28, 2026
9cbfca0
test: add tests for MFA APIs
tusharpandey13 Feb 1, 2026
efd883f
docs: update examples.md
tusharpandey13 Feb 3, 2026
e317aec
docs: update docstrings for mfa methods
tusharpandey13 Feb 3, 2026
9ae6b6a
fix: type changes: update casing for enroll options fields; add email…
tusharpandey13 Feb 3, 2026
12a9d8e
fix: refactor mfaVerify: decouple auth0 call and session caching
tusharpandey13 Feb 3, 2026
9f18dfe
feat: remove deleteAuthenticator impl
tusharpandey13 Feb 3, 2026
f498f52
Revert "chore: update AGENTS.md"
tusharpandey13 Feb 3, 2026
b32c677
Merge branch 'feat/mfa-base' into feat/mfa-apis
tusharpandey13 Feb 3, 2026
3274a4f
fix: segregate error classes to fix circular dependancy
tusharpandey13 Feb 8, 2026
76090c1
Merge branch 'feat/mfa-apis' of https://github.com/auth0/nextjs-auth0…
tusharpandey13 Feb 8, 2026
0edf469
Merge branch 'main' of https://github.com/auth0/nextjs-auth0 into fea…
tusharpandey13 Feb 9, 2026
6c7242d
feat: add MFA stepUpViaPopup
tusharpandey13 Feb 16, 2026
efac61a
docs: docs for mfa ul
tusharpandey13 Feb 16, 2026
3dc82bc
fix: refactor UL code, move static code to utils, fix some small issues
tusharpandey13 Feb 16, 2026
08f47c0
test: add tests for MFA UL flow
tusharpandey13 Feb 16, 2026
a3164bd
Merge branch 'main' into feat/mfa-ul
tusharpandey13 Feb 16, 2026
91955b4
fix: dont send prompt=login in MFA popup; reduce popup postMessage fa…
tusharpandey13 Feb 18, 2026
f9cca1e
Merge branch 'feat/mfa-ul' of https://github.com/auth0/nextjs-auth0 i…
tusharpandey13 Feb 18, 2026
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
235 changes: 233 additions & 2 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
- [Handling `MfaRequiredError`](#handling-mfarequirederror)
- [MFA Tenant Configuration](#mfa-tenant-configuration)
- [Critical Warning](#critical-warning)
- [Reactive MFA Step-Up (Popup)](#reactive-mfa-step-up-popup)
- [Overview](#overview-1)
- [Basic Usage](#basic-usage)
- [Handling MfaRequiredError from Client Components](#handling-mfarequirederror-from-client-components)
- [Configuration Options](#configuration-options)
- [CSP Nonce Support](#csp-nonce-support)
- [Error Handling](#error-handling-2)
- [Security Considerations](#security-considerations-1)
- [Known Limitations](#known-limitations)
- [Silent authentication](#silent-authentication)
- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession)
- [What is DPoP?](#what-is-dpop)
Expand Down Expand Up @@ -1278,8 +1287,9 @@ export async function GET() {
```

**Client Side:**
When the client receives the 403 with `mfa_required`, you should redirect the user to complete the step-up challenge.
When the client receives the 403 with `mfa_required`, you can either redirect the user to a dedicated MFA page or use the popup-based approach to complete MFA without a full-page redirect.

**Option 1: Full-page redirect**
```javascript
const response = await fetch("/api/protected");
if (response.status === 403) {
Expand All @@ -1292,6 +1302,10 @@ if (response.status === 403) {
}
```

**Option 2: Popup (no redirect)**

Use `mfa.stepUpWithPopup()` to complete MFA in a popup without leaving the current page. See [Reactive MFA Step-Up (Popup)](#reactive-mfa-step-up-popup) for full documentation.

### MFA Tenant Configuration

The SDK relies on background token refreshes to maintain user sessions. For these non-interactive requests to succeed, it is important to configure your MFA policies to allow `refresh_token` exchanges without immediate user challenge.
Expand Down Expand Up @@ -3545,4 +3559,221 @@ The SDK provides typed error classes for all MFA operations:
| `MfaVerifyError` | `invalid_grant` | Verification failed | Invalid OTP code |
| `MfaTokenNotFoundError` | `mfa_token_not_found` | No MFA context for token | Token not in session |
| `MfaTokenExpiredError` | `mfa_token_expired` | Token TTL exceeded | Context expired |
| `MfaTokenInvalidError` | `mfa_token_invalid` | Token tampered or wrong secret | Decryption failed |
| `MfaTokenInvalidError` | `mfa_token_invalid` | Token tampered or wrong secret | Decryption failed |

## Reactive MFA Step-Up (Popup)

### Overview

The SDK supports **reactive MFA step-up** via a browser popup using Auth0 Universal Login. When an API call fails with `mfa_required`, the client-side `mfa.stepUpWithPopup()` method opens a popup window where the user completes MFA through Auth0's Universal Login. After completion, the token is cached in the server-side session and returned directly to the caller — no full-page redirect required.

This is useful for applications that need to protect specific actions (e.g., transferring funds, changing settings) with MFA without disrupting the user's current page state.

**Flow summary:**
1. App calls an API that requires MFA → receives `MfaRequiredError`
2. App calls `mfa.stepUpWithPopup({ audience })` → popup opens
3. User completes MFA in the popup via Auth0 Universal Login
4. Popup sends result back via `postMessage` → popup auto-closes
5. SDK retrieves the cached token from the server session
6. `stepUpWithPopup()` resolves with the access token

### Basic Usage

```tsx
'use client';

import { mfa, getAccessToken } from '@auth0/nextjs-auth0/client';
import { MfaRequiredError } from '@auth0/nextjs-auth0/errors';
import { useState } from 'react';

export function ProtectedAction() {
const [result, setResult] = useState(null);
const [error, setError] = useState(null);

async function handleAction() {
try {
// 1. Try to get an access token for the protected API
const token = await getAccessToken({
audience: 'https://api.example.com',
scope: 'read:sensitive'
});

// 2. Use the token to call your API
const res = await fetch('https://api.example.com/sensitive', {
headers: { Authorization: `Bearer ${token}` }
});
setResult(await res.json());
} catch (err) {
if (err instanceof MfaRequiredError) {
try {
// 3. MFA required — trigger popup step-up
const { token } = await mfa.stepUpWithPopup({
audience: 'https://api.example.com',
scope: 'read:sensitive'
});

// 4. Retry with the step-up token
const res = await fetch('https://api.example.com/sensitive', {
headers: { Authorization: `Bearer ${token}` }
});
setResult(await res.json());
} catch (popupErr) {
setError(popupErr.message);
}
} else {
setError(err.message);
}
}
}

return (
<div>
<button onClick={handleAction}>Perform Sensitive Action</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</div>
);
}
```

### Handling MfaRequiredError from Client Components

The client-side `getAccessToken()` helper automatically detects 403 responses with `error: "mfa_required"` and throws `MfaRequiredError`. This allows you to use `instanceof` checks to trigger the popup flow:

```tsx
import { getAccessToken } from '@auth0/nextjs-auth0/client';
import { MfaRequiredError } from '@auth0/nextjs-auth0/errors';

try {
const token = await getAccessToken({ audience: 'https://api.example.com' });
} catch (err) {
if (err instanceof MfaRequiredError) {
// Trigger popup MFA step-up
const { token } = await mfa.stepUpWithPopup({
audience: 'https://api.example.com'
});
}
}
```

> [!NOTE]
> The `MfaRequiredError` detection works for both server-side and client-side `getAccessToken()` calls. On the client, it is reconstructed from the 403 JSON response returned by the `/auth/access-token` endpoint.

### Configuration Options

`stepUpWithPopup()` accepts the following options:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `audience` | `string` | *(required)* | Target API audience identifier |
| `scope` | `string` | `'openid profile email'` | Space-separated scopes for the token |
| `acr_values` | `string` | `'http://schemas.openid.net/pape/policies/2007/06/multi-factor'` | ACR values sent to Auth0 for step-up policy |
| `returnTo` | `string` | `'/'` | Return URL (used internally by the OAuth flow) |
| `timeout` | `number` | `60000` | Popup timeout in milliseconds |
| `popupWidth` | `number` | `400` | Popup window width in pixels |
| `popupHeight` | `number` | `600` | Popup window height in pixels |

**Example with custom options:**

```tsx
const { token } = await mfa.stepUpWithPopup({
audience: 'https://api.example.com',
scope: 'openid profile email transfer:funds',
timeout: 120000, // 2 minutes
popupWidth: 500,
popupHeight: 700
});
```

> [!NOTE]
> Popup timeout is configured per-call only. There is no server-side configuration option or environment variable for this — timeout is a client-side runtime concern. If you need a consistent default across your app, define an application-level constant and pass it to every call.

### CSP Nonce Support

If your application uses a strict Content Security Policy that blocks inline scripts, configure a CSP nonce on the server-side `Auth0Client`:

```typescript
// lib/auth0.ts
import { Auth0Client } from '@auth0/nextjs-auth0/server';

export const auth0 = new Auth0Client({
cspNonce: 'your-generated-nonce'
});
```

The nonce is injected into the `<script>` tag of the popup callback HTML response, making it compliant with `script-src 'nonce-...'` CSP policies.

> [!IMPORTANT]
> The nonce must contain only base64 characters (`A-Za-z0-9+/=-_`). Invalid characters will throw an `InvalidConfigurationError`. The nonce should be generated per-request on the server side for maximum security.

If you do **not** configure a `cspNonce` and your CSP blocks inline scripts, the popup will complete the MFA flow but the parent window will never receive the `postMessage`. This manifests as a `PopupTimeoutError` after the configured timeout.

### Error Handling

`stepUpWithPopup()` can throw several typed errors. Handle them to provide appropriate user feedback:

```tsx
import { mfa } from '@auth0/nextjs-auth0/client';
import {
PopupBlockedError,
PopupCancelledError,
PopupTimeoutError,
PopupInProgressError,
ExecutionContextError
} from '@auth0/nextjs-auth0/errors';

try {
const { token } = await mfa.stepUpWithPopup({
audience: 'https://api.example.com'
});
} catch (err) {
if (err instanceof PopupBlockedError) {
// Browser blocked the popup — prompt user to allow popups
alert('Please allow popups for this site and try again.');
} else if (err instanceof PopupCancelledError) {
// User closed the popup before completing MFA
console.log('MFA cancelled by user.');
} else if (err instanceof PopupTimeoutError) {
// Popup did not complete within the timeout
console.log('MFA timed out. Please try again.');
} else if (err instanceof PopupInProgressError) {
// Another popup is already open
console.log('Please complete the current MFA prompt first.');
} else if (err instanceof ExecutionContextError) {
// Called from server-side code (SSR, middleware)
console.error('stepUpWithPopup() can only be called in browser context.');
} else {
// AccessTokenError or other errors
console.error('MFA failed:', err.message);
}
}
```

**Error reference:**

| Error Class | Code | When Thrown |
|-------------|------|------------|
| `PopupBlockedError` | `popup_blocked` | Browser blocked `window.open()` |
| `PopupCancelledError` | `popup_cancelled` | User closed the popup window |
| `PopupTimeoutError` | `popup_timeout` | Popup did not complete within timeout |
| `PopupInProgressError` | `popup_in_progress` | Another `stepUpWithPopup()` call is active |
| `ExecutionContextError` | `invalid_execution_context` | Called outside browser context (SSR/middleware) |
| `AccessTokenError` | Various | Token retrieval failed after popup completed |

### Security Considerations

- **Same-origin postMessage:** The popup listener only accepts messages from `window.location.origin`. Cross-origin messages are silently ignored.
- **No tokens in postMessage:** The popup's `postMessage` payload contains only `{ sub, email }` metadata — never raw access tokens. Tokens remain server-side in the encrypted session cookie.
- **PKCE:** The popup flow uses the same PKCE-based authorization code exchange as standard login. No security downgrade.
- **State encryption:** The `returnStrategy` flag is stored in the encrypted transaction cookie alongside other OAuth state (AES-256-GCM).
- **XSS prevention:** The callback HTML uses `JSON.stringify()` with `<` escaping (`\u003c`) to prevent script injection via user-controlled values.

### Known Limitations

| Limitation | Details |
|------------|---------|
| **One popup at a time** | Only one `stepUpWithPopup()` call is allowed concurrently. A second call throws `PopupInProgressError` regardless of audience. |
| **Same-origin only** | The postMessage validation requires same-origin. Cross-origin popup flows are not supported. |
| **Browser popup policies** | Most browsers block popups unless triggered by a direct user action (click handler). Ensure `stepUpWithPopup()` is called within a user-initiated event handler. |
| **`beforeSessionSaved` idempotency** | The `beforeSessionSaved` hook runs again when the popup token is merged into the existing session. Ensure your hook is idempotent when using popup flows. |
| **Session cookie size** | Each cached MRRT token increases session cookie size. For applications with many audiences, consider using a [database session store](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#database-sessions). |
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ You can customize the client by using the options below:
| useDPoP | `boolean` | Enable DPoP (Demonstration of Proof-of-Possession) for enhanced security. When enabled, the client will generate DPoP proofs for token requests and protected resource requests. Defaults to `false`. |
| dpopKeyPair | `DpopKeyPair` | ES256 key pair for DPoP proof generation. If not provided, the SDK will attempt to load keys from `AUTH0_DPOP_PUBLIC_KEY` and `AUTH0_DPOP_PRIVATE_KEY` environment variables. Keys must be in PEM format. |
| dpopOptions | `DpopOptions` | Configure DPoP timing validation. Supports `clockSkew` (adjust assumed current time) and `clockTolerance` (validation tolerance). Can also be configured via `AUTH0_DPOP_CLOCK_SKEW` and `AUTH0_DPOP_CLOCK_TOLERANCE` environment variables. See [DPoP Clock Validation](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#dpop-clock-validation) for details. |
| cspNonce | `string` | CSP nonce for inline scripts in popup callback HTML responses. When provided, `<script>` tags in the MFA popup callback include a `nonce="..."` attribute for strict Content Security Policy compliance. Only required when using `mfa.stepUpWithPopup()` with a CSP that blocks inline scripts. |

### Customizing Auth Handlers

Expand Down
Loading