Skip to content

Commit 6e0c514

Browse files
authored
feat: add support for backchannel authentication (#2261)
1 parent 93e7b2a commit 6e0c514

File tree

8 files changed

+655
-53
lines changed

8 files changed

+655
-53
lines changed

EXAMPLES.md

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
- [Cookie Configuration](#cookie-configuration)
3333
- [Transaction Cookie Configuration](#transaction-cookie-configuration)
3434
- [Database sessions](#database-sessions)
35+
- [Back-Channel Authentication](#back-channel-authentication)
3536
- [Back-Channel Logout](#back-channel-logout)
3637
- [Combining middleware](#combining-middleware)
3738
- [ID Token claims and the user object](#id-token-claims-and-the-user-object)
@@ -48,8 +49,8 @@
4849
- [On the server (Pages Router)](#on-the-server-pages-router-3)
4950
- [Middleware](#middleware-3)
5051
- [Customizing Auth Handlers](#customizing-auth-handlers)
51-
- [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers)
52-
- [Run code after callback](#run-code-after-callback)
52+
- [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers)
53+
- [Run code after callback](#run-code-after-callback)
5354

5455
## Passing authorization parameters
5556

@@ -164,7 +165,7 @@ On the server, the `getSession()` helper can be used in Server Components, Serve
164165

165166
> [!NOTE]
166167
> The `getSession()` method returns a complete session object containing the user profile and all available tokens (access token, ID token, and refresh token when present). Use this method for applications that only need user identity information without calling external APIs, as it provides access to the user's profile data from the ID token without requiring additional API calls. This approach is suitable for session-only authentication patterns.
167-
For API access, use `getAccessToken()` to get an access token, this handles automatic token refresh.
168+
> For API access, use `getAccessToken()` to get an access token, this handles automatic token refresh.
168169
169170
```tsx
170171
import { auth0 } from "@/lib/auth0";
@@ -779,6 +780,7 @@ This will in turn, update the `access_token`, `id_token` and `expires_at` fields
779780
For applications where an API call might be made very close to the token's expiration time, network latency can cause the token to expire before the API receives it. To prevent this race condition, you can implement a strategy to refresh the token proactively when it's within a certain buffer period of its expiration.
780781

781782
The general approach is as follows:
783+
782784
1. Before making a sensitive API call, get the session and check the `expiresAt` timestamp from the `tokenSet`.
783785
2. Determine if the token is within your desired buffer period (e.g., 30-90 seconds) of expiring.
784786
3. If it is, force a token refresh by calling `auth0.getAccessToken({ refresh: true })`.
@@ -987,6 +989,7 @@ export const auth0 = new Auth0Client({
987989
## Transaction Cookie Configuration
988990

989991
### Customizing Transaction Cookie Expiration
992+
990993
You can configure transaction cookies expiration by providing a `maxAge` proeprty for `transactionCookie`.
991994

992995
```ts
@@ -997,11 +1000,13 @@ export const auth0 = new Auth0Client({
9971000
},
9981001
}
9991002
```
1003+
10001004
Transaction cookies are used to maintain state during authentication flows. The SDK provides several configuration options to manage transaction cookie behavior and prevent cookie accumulation issues.
10011005
10021006
### Transaction Management Modes
10031007
10041008
**Parallel Transactions (Default)**
1009+
10051010
```ts
10061011
const authClient = new Auth0Client({
10071012
enableParallelTransactions: true // Default: allows multiple concurrent logins
@@ -1010,6 +1015,7 @@ const authClient = new Auth0Client({
10101015
```
10111016
10121017
**Single Transaction Mode**
1018+
10131019
```ts
10141020
const authClient = new Auth0Client({
10151021
enableParallelTransactions: false // Only one active transaction at a time
@@ -1018,25 +1024,27 @@ const authClient = new Auth0Client({
10181024
```
10191025
10201026
**Use Parallel Transactions (Default) When:**
1027+
10211028
- Users might open multiple tabs and attempt to log in simultaneously
10221029
- You want maximum compatibility with typical user behavior
10231030
- Your application supports multiple concurrent authentication flows
10241031
10251032
**Use Single Transaction Mode When:**
1033+
10261034
- You want to prevent cookie accumulation issues in applications with frequent login attempts
10271035
- You prefer simpler transaction management
10281036
- Users typically don't need multiple concurrent login flows
10291037
- You're experiencing cookie header size limits due to abandoned transaction cookies edge cases
10301038
10311039
### Transaction Cookie Options
10321040
1033-
| Option | Type | Description |
1034-
| -------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1035-
| cookieOptions.maxAge | `number` | The expiration time for transaction cookies in seconds. Defaults to `3600` (1 hour). After this time, abandoned transaction cookies will expire automatically. |
1036-
| cookieOptions.prefix | `string` | The prefix for transaction cookie names. Defaults to `__txn_`. In parallel mode, cookies are named `__txn_{state}`. In single mode, just `__txn_`. |
1037-
| cookieOptions.sameSite | `"strict" \| "lax" \| "none"` | Controls when the cookie is sent with cross-site requests. Defaults to `"lax"`. |
1038-
| cookieOptions.secure | `boolean` | When `true`, the cookie will only be sent over HTTPS connections. Automatically determined based on your application's base URL protocol if not specified. |
1039-
| cookieOptions.path | `string` | Specifies the URL path for which the cookie is valid. Defaults to `"/"`. |
1041+
| Option | Type | Description |
1042+
| ---------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1043+
| cookieOptions.maxAge | `number` | The expiration time for transaction cookies in seconds. Defaults to `3600` (1 hour). After this time, abandoned transaction cookies will expire automatically. |
1044+
| cookieOptions.prefix | `string` | The prefix for transaction cookie names. Defaults to `__txn_`. In parallel mode, cookies are named `__txn_{state}`. In single mode, just `__txn_`. |
1045+
| cookieOptions.sameSite | `"strict" \| "lax" \| "none"` | Controls when the cookie is sent with cross-site requests. Defaults to `"lax"`. |
1046+
| cookieOptions.secure | `boolean` | When `true`, the cookie will only be sent over HTTPS connections. Automatically determined based on your application's base URL protocol if not specified. |
1047+
| cookieOptions.path | `string` | Specifies the URL path for which the cookie is valid. Defaults to `"/"`. |
10401048
10411049
## Database sessions
10421050
@@ -1063,6 +1071,28 @@ export const auth0 = new Auth0Client({
10631071
});
10641072
```
10651073
1074+
## Using Client-Initiated Backchannel Authentication
1075+
1076+
Using Client-Initiated Backchannel Authentication can be done by calling `getTokenByBackchannelAuth()`:
1077+
1078+
```ts
1079+
import { auth0 } from "@/lib/auth0";
1080+
1081+
const tokenResponse = await auth0.getTokenByBackchannelAuth({
1082+
bindingMessage: "",
1083+
loginHint: {
1084+
sub: "auth0|123456789"
1085+
}
1086+
});
1087+
```
1088+
1089+
- `bindingMessage`: A human-readable message to be displayed at the consumption device and authentication device. This allows the user to ensure the transaction initiated by the consumption device is the same that triggers the action on the authentication device.
1090+
- `loginHint.sub`: The `sub` claim of the user that is trying to login using Client-Initiated Backchannel Authentication, and to which a push notification to authorize the login will be sent.
1091+
1092+
> [!IMPORTANT]
1093+
> Using Client-Initiated Backchannel Authentication requires the feature to be enabled in the Auth0 dashboard.
1094+
> Read [the Auth0 docs](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) to learn more about Client-Initiated Backchannel Authentication.
1095+
10661096
## Back-Channel Logout
10671097
10681098
The SDK can be configured to listen to [Back-Channel Logout](https://auth0.com/docs/authenticate/login/logout/back-channel-logout) events. By default, a route will be mounted `/auth/backchannel-logout` which will verify the logout token and call the `deleteByLogoutToken` method of your session store implementation to allow you to remove the session.
@@ -1424,6 +1454,7 @@ export async function middleware(request: NextRequest) {
14241454
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.
14251455
14261456
This approach allows you to:
1457+
14271458
- Run custom code before authentication actions (logging, analytics, validation)
14281459
- Modify the response (set cookies, headers, etc.)
14291460
- Implement custom redirects or early returns when needed
@@ -1435,49 +1466,48 @@ The middleware-based approach provides the same level of control as v3's custom
14351466
### Run custom code before Auth Handlers
14361467
14371468
Following example shows how to run custom logic before the response of `logout` handler is returned:
1469+
14381470
```ts
14391471
export async function middleware(request) {
1472+
// prepare NextResponse object from auth0 middleware
1473+
const authRes = await auth0.middleware(request);
14401474

1441-
// prepare NextResponse object from auth0 middleware
1442-
const authRes = await auth0.middleware(request);
1443-
1444-
// The following interceptUrls can be used:
1445-
// "/auth/login" : intercept login auth handler
1446-
// "/auth/logout" : intercept logout auth handler
1447-
// "/auth/callback" : intercept callback auth handler
1448-
// "/your/login/returnTo/url" : intercept redirect after login, this is the login returnTo url
1449-
// "/your/logout/returnTo/url" : intercept redirect after logout, this is the logout returnTo url
1450-
1451-
const interceptUrl = "/auth/logout";
1452-
1453-
// intercept auth handler
1454-
if (request.nextUrl.pathname === interceptUrl) {
1455-
// do custom stuff
1456-
console.log("Pre-logout code")
1457-
1458-
// Example: Set a cookie
1459-
authRes.cookies.set('myCustomCookie', 'cookieValue', { path: '/' });
1460-
// Example: Set another cookie with options
1461-
authRes.cookies.set({
1462-
name: 'anotherCookie',
1463-
value: 'anotherValue',
1464-
httpOnly: true,
1465-
path: '/',
1466-
});
1467-
1468-
// Example: Delete a cookie
1469-
// authRes.cookies.delete('cookieNameToDelete');
1470-
1471-
// you can also do an early return here with your own NextResponse object
1472-
// return NextResponse.redirect(new URL('/custom-logout-page'));
1473-
}
1475+
// The following interceptUrls can be used:
1476+
// "/auth/login" : intercept login auth handler
1477+
// "/auth/logout" : intercept logout auth handler
1478+
// "/auth/callback" : intercept callback auth handler
1479+
// "/your/login/returnTo/url" : intercept redirect after login, this is the login returnTo url
1480+
// "/your/logout/returnTo/url" : intercept redirect after logout, this is the logout returnTo url
1481+
1482+
const interceptUrl = "/auth/logout";
1483+
1484+
// intercept auth handler
1485+
if (request.nextUrl.pathname === interceptUrl) {
1486+
// do custom stuff
1487+
console.log("Pre-logout code");
1488+
1489+
// Example: Set a cookie
1490+
authRes.cookies.set("myCustomCookie", "cookieValue", { path: "/" });
1491+
// Example: Set another cookie with options
1492+
authRes.cookies.set({
1493+
name: "anotherCookie",
1494+
value: "anotherValue",
1495+
httpOnly: true,
1496+
path: "/"
1497+
});
14741498

1475-
// return the original auth0-handled NextResponse object
1476-
return authRes
1499+
// Example: Delete a cookie
1500+
// authRes.cookies.delete('cookieNameToDelete');
1501+
1502+
// you can also do an early return here with your own NextResponse object
1503+
// return NextResponse.redirect(new URL('/custom-logout-page'));
1504+
}
1505+
1506+
// return the original auth0-handled NextResponse object
1507+
return authRes;
14771508
}
14781509
```
14791510
14801511
### Run code after callback
1481-
Please refer to [onCallback](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#oncallback)
1482-
for details on how to run code after callback.
1483-
```
1512+
1513+
Please refer to [onCallback](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#oncallback) for details on how to run code after callback.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"@panva/hkdf": "^1.2.1",
8686
"jose": "^6.0.11",
8787
"oauth4webapi": "^3.1.2",
88+
"openid-client": "^6.6.2",
8889
"swr": "^2.2.5"
8990
},
9091
"publishConfig": {

pnpm-lock.yaml

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/errors/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,30 @@ export class BackchannelLogoutError extends SdkError {
9595
}
9696
}
9797

98+
export class BackchannelAuthenticationNotSupportedError extends SdkError {
99+
public code: string = "backchannel_authentication_not_supported_error";
100+
101+
constructor() {
102+
super(
103+
"The authorization server does not support backchannel authentication. Learn how to enable it here: https://auth0.com/docs/get-started/applications/configure-client-initiated-backchannel-authentication"
104+
);
105+
this.name = "BackchannelAuthenticationNotSupportedError";
106+
}
107+
}
108+
109+
export class BackchannelAuthenticationError extends SdkError {
110+
public code: string = "backchannel_authentication_error";
111+
public cause?: OAuth2Error;
112+
113+
constructor({ cause }: { cause?: OAuth2Error }) {
114+
super(
115+
"There was an error when trying to use Client-Initiated Backchannel Authentication."
116+
);
117+
this.cause = cause;
118+
this.name = "BackchannelAuthenticationError";
119+
}
120+
}
121+
98122
export enum AccessTokenErrorCode {
99123
MISSING_SESSION = "missing_session",
100124
MISSING_REFRESH_TOKEN = "missing_refresh_token",

0 commit comments

Comments
 (0)