Skip to content

Commit 4a2f36e

Browse files
feat: Custom Token Exchange (#2453)
2 parents f435994 + a2e14d2 commit 4a2f36e

File tree

7 files changed

+1365
-1
lines changed

7 files changed

+1365
-1
lines changed

EXAMPLES.md

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@
117117
- [On the server (App Router)](#on-the-server-app-router-3)
118118
- [On the server (Pages Router)](#on-the-server-pages-router-3)
119119
- [Middleware](#middleware-3)
120+
- [Custom Token Exchange](#custom-token-exchange)
121+
- [When to Use](#when-to-use)
122+
- [Basic Usage](#basic-usage)
123+
- [With Organization](#with-organization)
124+
- [With Actor Token (Delegation)](#with-actor-token-delegation)
125+
- [Error Handling](#error-handling-2)
126+
- [Token Type Requirements](#token-type-requirements)
127+
- [Limitations](#limitations)
128+
- [DPoP Support](#dpop-support)
120129
- [Customizing Auth Handlers](#customizing-auth-handlers)
121130
- [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers)
122131
- [Run code after callback](#run-code-after-callback)
@@ -1340,7 +1349,7 @@ Handle DPoP-specific errors gracefully with proper error detection and response
13401349
Implement comprehensive error handling for DPoP configuration and runtime issues:
13411350

13421351
```typescript
1343-
import { DPoPError, DPoPErrorCode } from "@auth0/nextjs-auth0/server";
1352+
import { DPoPError, DPoPErrorCode } from "@auth0/nextjs-auth0/errors";
13441353

13451354
import { auth0 } from "@/lib/auth0";
13461355

@@ -2751,6 +2760,151 @@ export async function middleware(request: NextRequest) {
27512760
}
27522761
```
27532762
2763+
## Custom Token Exchange
2764+
2765+
Custom Token Exchange (CTE) allows you to exchange external tokens (from legacy systems, third-party identity providers, or custom token services) for Auth0 access tokens. This implements [RFC 8693 (OAuth 2.0 Token Exchange)](https://datatracker.ietf.org/doc/html/rfc8693).
2766+
2767+
### When to Use
2768+
2769+
- **Legacy System Migration**: Exchange tokens from legacy auth systems for Auth0 tokens
2770+
- **Third-Party Federation**: Convert tokens from external identity providers
2771+
- **Token Mediation**: Bridge between different token ecosystems in your architecture
2772+
2773+
### Basic Usage
2774+
2775+
```ts
2776+
import { auth0 } from "@/lib/auth0";
2777+
2778+
export async function exchangeExternalToken(legacyToken: string) {
2779+
try {
2780+
const result = await auth0.customTokenExchange({
2781+
subjectToken: legacyToken,
2782+
subjectTokenType: "urn:acme:legacy-token",
2783+
audience: "https://api.example.com"
2784+
});
2785+
2786+
return {
2787+
accessToken: result.accessToken,
2788+
expiresIn: result.expiresIn,
2789+
tokenType: result.tokenType
2790+
};
2791+
} catch (error) {
2792+
if (error instanceof CustomTokenExchangeError) {
2793+
console.error(`Exchange failed: ${error.code}`, error.message);
2794+
}
2795+
throw error;
2796+
}
2797+
}
2798+
```
2799+
2800+
### With Organization
2801+
2802+
When exchanging tokens for organization-scoped access:
2803+
2804+
```ts
2805+
const result = await auth0.customTokenExchange({
2806+
subjectToken: externalToken,
2807+
subjectTokenType: "urn:partner:sso-token",
2808+
organization: "org_abc123",
2809+
scope: "read:data write:data"
2810+
});
2811+
```
2812+
2813+
### With Actor Token (Delegation)
2814+
2815+
For delegation scenarios where a service acts on behalf of a user:
2816+
2817+
```ts
2818+
const result = await auth0.customTokenExchange({
2819+
subjectToken: userToken,
2820+
subjectTokenType: "urn:acme:user-token",
2821+
actorToken: serviceToken,
2822+
actorTokenType: "urn:acme:service-token",
2823+
audience: "https://downstream-api.example.com"
2824+
});
2825+
```
2826+
2827+
### Error Handling
2828+
2829+
```ts
2830+
import {
2831+
CustomTokenExchangeError,
2832+
CustomTokenExchangeErrorCode
2833+
} from "@auth0/nextjs-auth0/errors";
2834+
2835+
try {
2836+
const result = await auth0.customTokenExchange({
2837+
subjectToken: token,
2838+
subjectTokenType: "urn:acme:token"
2839+
});
2840+
} catch (error) {
2841+
if (error instanceof CustomTokenExchangeError) {
2842+
switch (error.code) {
2843+
case CustomTokenExchangeErrorCode.MISSING_SUBJECT_TOKEN:
2844+
// Handle missing subject token
2845+
break;
2846+
case CustomTokenExchangeErrorCode.INVALID_SUBJECT_TOKEN_TYPE:
2847+
// Handle invalid token type format
2848+
break;
2849+
case CustomTokenExchangeErrorCode.MISSING_ACTOR_TOKEN_TYPE:
2850+
// Handle missing actor token type when actor token provided
2851+
break;
2852+
case CustomTokenExchangeErrorCode.EXCHANGE_FAILED:
2853+
// Handle server-side exchange failure
2854+
console.error("Exchange failed:", error.cause);
2855+
break;
2856+
}
2857+
}
2858+
}
2859+
```
2860+
2861+
### Token Type Requirements
2862+
2863+
The `subjectTokenType` (and `actorTokenType` if used) must:
2864+
2865+
- Be 10-100 characters in length (per [Auth0 CTE Profiles Management API](https://auth0.com/docs/api/management/v2#!/Token_Exchange_Profiles))
2866+
- Be a valid URI (starting with `urn:` or `https://` or `http://`)
2867+
2868+
Valid examples:
2869+
2870+
- `urn:acme:legacy-token`
2871+
- `urn:partner:sso-token:v1`
2872+
- `https://example.com/token-types/external`
2873+
2874+
> **Note**: Reserved namespaces (e.g., `urn:ietf:`, `urn:auth0:`) are validated by Auth0 when creating CTE profiles via the Management API.
2875+
2876+
### Limitations
2877+
2878+
> [!IMPORTANT]
2879+
> Custom Token Exchange has specific constraints you should be aware of (see [Auth0 Custom Token Exchange documentation](https://auth0.com/docs/authenticate/custom-token-exchange) for details):
2880+
2881+
- **Server-side only**: Requires `client_secret`, cannot be used in browser
2882+
- **No Auth0 session created**: Returns tokens only, does not establish an Auth0 session
2883+
- **No token caching**: Tokens are not stored in the user's session; each call performs a new exchange
2884+
- **MFA not supported**: Exchange fails if the user's policy requires MFA
2885+
- **Rate limiting**: Subject to Auth0's token exchange rate limits
2886+
2887+
### DPoP Support
2888+
2889+
When DPoP is enabled in your Auth0Client configuration, custom token exchange automatically uses DPoP-bound tokens:
2890+
2891+
```ts
2892+
const auth0 = new Auth0Client({
2893+
// ... other config
2894+
dPoPOptions: {
2895+
enabled: true
2896+
}
2897+
});
2898+
2899+
// DPoP proof will be automatically included
2900+
const result = await auth0.customTokenExchange({
2901+
subjectToken: externalToken,
2902+
subjectTokenType: "urn:acme:external-token"
2903+
});
2904+
2905+
// result.tokenType will be "DPoP"
2906+
```
2907+
27542908
## Customizing Auth Handlers
27552909

27562910
Authentication routes (`/auth/login`, `/auth/logout`, `/auth/callback`) are handled automatically by the middleware. You can intercept these routes in your middleware to run custom logic before the auth handlers execute.

src/errors/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,65 @@ export class AccessTokenForConnectionError extends SdkError {
192192
}
193193
}
194194

195+
/**
196+
* Error codes for Custom Token Exchange errors.
197+
*/
198+
export enum CustomTokenExchangeErrorCode {
199+
/**
200+
* The subject_token is missing or empty.
201+
*/
202+
MISSING_SUBJECT_TOKEN = "missing_subject_token",
203+
204+
/**
205+
* The subject_token_type is not a valid URI, wrong length, or uses a reserved namespace.
206+
*/
207+
INVALID_SUBJECT_TOKEN_TYPE = "invalid_subject_token_type",
208+
209+
/**
210+
* The actor_token was provided without actor_token_type.
211+
*/
212+
MISSING_ACTOR_TOKEN_TYPE = "missing_actor_token_type",
213+
214+
/**
215+
* The token exchange request failed.
216+
*/
217+
EXCHANGE_FAILED = "exchange_failed"
218+
}
219+
220+
/**
221+
* Error class representing a Custom Token Exchange error.
222+
* Extends the `SdkError` class.
223+
*
224+
* This error is thrown when a Custom Token Exchange operation fails,
225+
* such as validation errors or server-side token exchange failures.
226+
*
227+
* @see {@link https://auth0.com/docs/authenticate/custom-token-exchange Auth0 Custom Token Exchange Documentation}
228+
*/
229+
export class CustomTokenExchangeError extends SdkError {
230+
/**
231+
* The error code associated with the custom token exchange error.
232+
*/
233+
public code: string;
234+
/**
235+
* The underlying OAuth2 error that caused this error (if applicable).
236+
*/
237+
public cause?: OAuth2Error;
238+
239+
/**
240+
* Constructs a new `CustomTokenExchangeError` instance.
241+
*
242+
* @param code - The error code.
243+
* @param message - The error message.
244+
* @param cause - The OAuth2 cause of the error.
245+
*/
246+
constructor(code: string, message: string, cause?: OAuth2Error) {
247+
super(message);
248+
this.name = "CustomTokenExchangeError";
249+
this.code = code;
250+
this.cause = cause;
251+
}
252+
}
253+
195254
/**
196255
* Error codes for DPoP-related errors.
197256
*

0 commit comments

Comments
 (0)