Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
195 changes: 195 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Examples

- [Passing authorization parameters](#passing-authorization-parameters)
- [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa)
- [Step-up Authentication](#step-up-authentication)
- [Handling `MfaRequiredError`](#handling-mfarequirederror)
- [MFA Tenant Configuration](#mfa-tenant-configuration)
- [Example Scenarios](#example-scenarios)
- [The `returnTo` parameter](#the-returnto-parameter)
- [Redirecting the user after authentication](#redirecting-the-user-after-authentication)
- [Redirecting the user after logging out](#redirecting-the-user-after-logging-out)
Expand Down Expand Up @@ -40,6 +45,11 @@
- [Usage Example](#usage-example)
- [Token Management Best Practices](#token-management-best-practices)
- [Mitigating Token Expiration Race Conditions in Latency-Sensitive Operations](#mitigating-token-expiration-race-conditions-in-latency-sensitive-operations)
- [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa)
- [Step-up Authentication](#step-up-authentication)
- [Handling `MfaRequiredError`](#handling-mfarequirederror)
- [MFA Tenant Configuration](#mfa-tenant-configuration)
- [Critical Warning](#critical-warning)
- [Silent authentication](#silent-authentication)
- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession)
- [What is DPoP?](#what-is-dpop)
Expand Down Expand Up @@ -1204,6 +1214,93 @@ This ensures that the token you send is guaranteed to be valid for at least the
> [!IMPORTANT]
> This strategy is **not** a solution for long-running operations that take longer than the token's total validity period (e.g., 10 minutes). In those cases, the token will still expire mid-operation. The correct approach for long-running tasks is to call `getAccessToken()` immediately before the operation that requires it, ensuring you have a fresh token. The buffer is only for mitigating latency-related failures in short-lived requests.

## Multi-Factor Authentication (MFA)

Start by checking out the [MFA example app](./examples/mfa) for a complete working demo.

### Step-up Authentication

Step-up authentication is a pattern where an application allows access to some resources with potential sensitive data, but requires the user to authenticate with a stronger mechanism (like MFA) to access others.

The SDK supports handling the `mfa_required` error from Auth0 when an API requires higher security. This typically happens when you use an Auth0 Action or Rule to enforce MFA for specific audiences or scopes.

### Handling `MfaRequiredError`

When you request an Access Token for a resource that requires MFA, Auth0 will return a `403 Forbidden` with an `mfa_required` error code. The SDK automatically catches this and bubbles it up as an `MfaRequiredError`, containing the `mfa_token` needed to resolve the challenge.

You should catch this error in your API routes or Server Actions and forward the `mfa_token` to your client.

**Server Side (API Route):**
```javascript
import { NextResponse } from "next/server";
import { auth0 } from "@/lib/auth0";
import { MfaRequiredError } from "@auth0/nextjs-auth0/server";

export async function GET() {
try {
const { token } = await auth0.getAccessToken({
audience: "https://my-high-security-api",
refresh: true // Ensure we get a fresh token check
});
return NextResponse.json({ token });
} catch (error) {
if (error instanceof MfaRequiredError) {
// Forward the error details to the client
return NextResponse.json(error.toJSON(), { status: 403 });
}
throw error;
}
}
```

**Client Side:**
When the client receives the 403 with `mfa_required`, you should redirect the user to complete the step-up challenge.

```javascript
const response = await fetch("/api/protected");
if (response.status === 403) {
const data = await response.json();
if (data.error === "mfa_required") {
// Redirect to your MFA page or show MFA prompt
// Pass the mfa_token to the challenge flow
window.location.href = `/mfa-challenge?token=${data.mfa_token}`;
}
}
```

### 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.

Enforcing **"Always"** or **"All Applications"** in your global Tenant MFA Policy will block these background requests, as they cannot satisfy an interactive MFA challenge.

**Recommended Configuration:**
1. Set Tenant MFA Policy to **"Adaptive"** or **"Never"**.
2. Use **Auth0 Actions** to enforce MFA conditionally (only when specific resources are requested).
3. Ensure your Action explicitly skips MFA for `refresh_token` grants.

**Example Action Code:**
```javascript
exports.onExecutePostLogin = async (event, api) => {
// 1. SKIP MFA for silent token refreshes
if (event.request.body.grant_type === 'refresh_token') {
return;
}

// 2. Enforce only for specific audience
const targetAudience = 'https://my-high-security-api';
const requestedAudience = event.transaction?.requested_audience || [];

if (requestedAudience.includes(targetAudience)) {
// 3. Ensure user has enrolled factors before challenging
// (Otherwise they get a 500 error page)
if (event.user.multifactor && event.user.multifactor.length > 0) {
api.authentication.challengeWith({ type: 'otp' });
}
}
};
```

## Silent authentication

Silent authentication checks for an existing Auth0 session without user interaction. Use `prompt: 'none'` as an authorization parameter.
Expand Down Expand Up @@ -1244,6 +1341,104 @@ try {
}
```

## MFA Step-Up Authentication

MFA Step-Up allows your application to require additional authentication factors for sensitive operations, even when the user is already logged in.

### When MFA Step-Up Occurs

When calling `getAccessToken()` with a token request that requires MFA (due to Auth0 policies like Actions or APIs requiring specific ACR values), the SDK throws an `MfaRequiredError` containing:

- An **encrypted** `mfa_token` for completing MFA
- `mfa_requirements` describing available challenge/enrollment options
- The original `error_description` from Auth0

### Handling MfaRequiredError

```typescript
import { auth0, MfaRequiredError } from '@auth0/nextjs-auth0/server';

export async function performSensitiveAction() {
try {
const { token } = await auth0.getAccessToken({
audience: 'https://sensitive-api.example.com',
scope: 'admin:delete'
});

// Use token for sensitive operation
return await callSensitiveApi(token);
} catch (error) {
if (error instanceof MfaRequiredError) {
// MFA is required - the encrypted mfa_token is available
console.log('MFA required:', error.mfa_requirements);

// Option 1: Return error to client for handling
return Response.json(error.toJSON(), { status: 403 });

// Option 2: Redirect to custom MFA flow
// return NextResponse.redirect('/mfa-challenge');
}
throw error;
}
}
```

### API Route Handler Example

```typescript
// app/api/sensitive/route.ts
import { auth0, MfaRequiredError } from '@auth0/nextjs-auth0/server';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
try {
const { token } = await auth0.getAccessToken();
// Perform sensitive operation
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof MfaRequiredError) {
// Return 403 with MFA details for client to handle
return NextResponse.json(error.toJSON(), { status: 403 });
}
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
}
```

### MFA Error Types

| Error Class | Code | When Thrown |
|-------------|------|-------------|
| `MfaRequiredError` | `mfa_required` | Token refresh requires MFA step-up |
| `MfaTokenNotFoundError` | `mfa_token_not_found` | No MFA context for provided token |
| `MfaTokenExpiredError` | `mfa_token_expired` | Encrypted MFA token TTL exceeded |
| `MfaTokenInvalidError` | `mfa_token_invalid` | Token tampered or wrong secret |

### Configuration

Configure MFA token TTL via options or environment variable:

```typescript
// Option 1: Via constructor
const auth0 = new Auth0Client({
mfaContextTtl: 600 // 10 minutes in seconds
});
```

```bash
# Option 2: Via environment variable
AUTH0_MFA_CONTEXT_TTL=600
```

Default TTL is 300 seconds (5 minutes), matching Auth0's mfa_token expiration.

### Session Context

When MFA is required, the SDK automatically stores MFA context in the session keyed by a hash of the raw token.

> [!NOTE]
> The MFA context is cleaned up automatically when the session is written. Expired contexts (based on `mfaContextTtl`) are removed to prevent session bloat.

## DPoP (Demonstrating Proof-of-Possession)

DPoP is an OAuth 2.0 extension that enhances security by binding access tokens to a client's private key. This prevents token theft and replay attacks by requiring cryptographic proof that the client possessing the token also possesses the private key used to request it.
Expand Down
12 changes: 12 additions & 0 deletions examples/mfa/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
AUTH0_SECRET=use_a_long_random_string_here
APP_BASE_URL=http://localhost:3000

AUTH0_ISSUER_BASE_URL='https://YOUR_DOMAIN.auth0.com'
AUTH0_DOMAIN=YOUR_DOMAIN.auth0.com
AUTH0_CLIENT_ID='YOUR_CLIENT_ID'
AUTH0_CLIENT_SECRET='YOUR_CLIENT_SECRET'
AUTH0_AUDIENCE='resource-server-1'
AUTH0_SCOPE='openid profile email offline_access'

# MFA Configuration
AUTH0_MFA_CONTEXT_TTL=300
33 changes: 33 additions & 0 deletions examples/mfa/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
!.env.local.example
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
114 changes: 114 additions & 0 deletions examples/mfa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Next.js MFA Step-up Authentication Example

This example demonstrates how to implement Step-up Authentication (MFA) for specific resources using `@auth0/nextjs-auth0` and Auth0 Actions. It handles the `mfa_required` error and ensures strict MFA enforcement without breaking standard session refreshes.

## Prerequisites

- An Auth0 Tenant.
- A basic understanding of [Auth0 Actions](https://auth0.com/docs/customize/actions).

## ⚠️ Critical Configuration Warnings

1. **Tenant MFA Policy**: Set your Global MFA Policy to **"Adaptive"** or **"Disabled"**.
* **Do NOT** use "Always" or "All Applications" globally. This enforces MFA on `refresh_token` exchanges (background updates), which will break your application's session management and cause infinite loops in Next.js.
2. **Enforcement via Actions**: We use an Auth0 Action to enforce MFA only when specific conditions are met (e.g., requesting a specific Audience or Scope).

## Setup Instructions

### 1. Configure Auth0 Action

The "Gold Standard" for Step-up Authentication requires handling edge cases that standard documentation often overlooks.

Create a new **Post Login** Action in your Auth0 Dashboard with the following code.

**Why this code?**
* **Refresh Grant Guard**: `if (grantType === 'refresh_token') return;` is critical. Without it, silent token refreshes will fail.
* **Enrollment Check**: Checks if the user has factors enrolled before challenging. Calling `challengeWith` on an unenrolled user causes a 500 server error ("Something Went Wrong").

```javascript
/**
* Handler that will be called during the execution of a PostLogin flow.
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
// 1. CRITICAL: Skip MFA for refresh token grants to prevent loops/errors in background updates
const grantType = event.request?.body?.grant_type;
if (grantType === 'refresh_token') {
return;
}

// 2. Define target (Audience or Scope)
// Adjust this to match the audience requested by your app
const targetAudience = event.secrets.TARGET_AUDIENCE || 'resource-server-1';

const requestedAudience = event.transaction?.requested_audience ||
event.request?.query?.audience ||
(event.resource_server?.identifier);

// 3. Check if target is being requested
if (!requestedAudience || !requestedAudience.includes(targetAudience)) {
return;
}

// 4. Check if MFA already completed in this session
const authMethods = event.authentication?.methods || [];
const hasMfa = authMethods.some(method => method.name === 'mfa');
if (hasMfa) {
return;
}

// 5. Check if user is enrolled
const enrolledFactors = event.user.multifactor || [];
if (enrolledFactors.length === 0) {
// Option: Force enrollment (api.authentication.enrollWith) or Fail
// For this test setup, we skip to avoid "Something went wrong" errors for new users
console.log('User has no MFA enrolled, skipping challenge');
return;
}

// 6. Challenge
api.authentication.challengeWith({ type: 'otp' });
};
```

**Deploy the Action** and add it to your "Login" flow.

### 2. Configure Environment Variables

Copy the example environment file and configure your Auth0 credentials.

```bash
cp .env.local.example .env.local
```

Ensure your `AUTH0_ISSUER_BASE_URL`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` are set.
For this example to work as intended, ensure your application requests the Audience defined in your Action logic (e.g., `AUTH0_AUDIENCE=resource-server-1`).

### 3. Run the Application

```bash
npm install
npm run dev
```

Visit `http://localhost:3000`.

## How It Works

1. **Initial Login**: The user logs in. If they don't request the sensitive scope/audience, no MFA is required.
2. **Step-up Request**: When the user accesses a protected route or clicks a button that requests an Access Token for the sensitive audience:
* The SDK calls `getAccessToken`.
* Auth0 Action triggers and denies the token with `mfa_required`.
3. **Error Handling**:
* The SDK catches the `MfaRequiredError`.
* The error object contains an `mfa_token`.
* The app redirects the user back to Auth0, passing this `mfa_token` to the `/authorize` endpoint.
4. **Verification**: The user enters their OTP code.
5. **Success**: Auth0 redirects back to the app with the step-up completed. The `getAccessToken` call now succeeds.

## Troubleshooting & Common Pitfalls

- **"Something Went Wrong" on login**: The Action likely called `challengeWith` for a user who hasn't enrolled in MFA yet. Ensure your test user has enrolled manually (e.g., via Account Settings or a separate flow) before testing the step-up. The provided Action code defensively skips this challenge to prevent this error.
- **Endless interactions/Validation errors**: Check that your "Global Policy" in Auth0 is NOT set to "Always". It must be "Adaptive" or "Disabled" so the Action controls the logic.
- **Refresh Token fails**: Ensure the Action includes the `if (grantType === 'refresh_token') return;` check. Background requests for tokens must be allowed to proceed without interaction.
Loading