Skip to content

bugfix: Prevent Infinite Transaction Cookie Accumulation + Configuration Options #2236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
133 changes: 133 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [`onCallback`](#oncallback)
- [Session configuration](#session-configuration)
- [Cookie Configuration](#cookie-configuration)
- [Transaction Cookie Configuration](#transaction-cookie-configuration)
- [Database sessions](#database-sessions)
- [Back-Channel Logout](#back-channel-logout)
- [Combining middleware](#combining-middleware)
Expand All @@ -49,6 +50,7 @@
- [Customizing Auth Handlers](#customizing-auth-handlers)
- [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers)
- [Run code after callback](#run-code-after-callback)
- [Troubleshooting Cookie Issues](#troubleshooting-cookie-issues)

## Passing authorization parameters

Expand Down Expand Up @@ -880,6 +882,66 @@ export const auth0 = new Auth0Client({
});
```

## Troubleshooting Cookie Issues

### HTTP 413 Request Entity Too Large Errors

If you're experiencing HTTP 413 errors during authentication, this is likely due to transaction cookie accumulation. Transaction cookies (`__txn_*`) are created for each authentication attempt and can accumulate if not properly cleaned up.

**Solution**: Configure single transaction mode to prevent cookie accumulation:

```ts
import { TransactionStore } from "@auth0/nextjs-auth0/server";

const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
enableParallelTransactions: false, // Prevents cookie accumulation
cookieOptions: {
maxAge: 1800 // 30 minutes
}
});

export const auth0 = new Auth0Client({
transactionStore,
// ... other options
});
```

### Too Many Transaction Cookies

If you see multiple `__txn_*` cookies in your browser's developer tools:

1. **For parallel transactions (default)**: This is normal behavior that supports multi-tab login flows
2. **To reduce cookie count**: Switch to single transaction mode as shown above
3. **Automatic cleanup**: Cookies will be cleaned up automatically after successful authentication or when they expire

### Cookie Expiration Issues

Transaction cookies expire after 1 hour by default. If you need different expiration times:

```ts
const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
cookieOptions: {
maxAge: 3600 // Customize expiration time in seconds
}
});
```

### Development vs Production Cookie Settings

Different cookie settings may be needed for development and production:

```ts
const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
cookieOptions: {
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "lax" : "lax"
}
});
```

## Session configuration

The session configuration can be managed by specifying a `session` object when configuring the Auth0 client, like so:
Expand Down Expand Up @@ -961,6 +1023,77 @@ export const auth0 = new Auth0Client({
> [!INFO]
> The `httpOnly` attribute for the session cookie is always set to `true` for security reasons and cannot be configured via options or environment variables.

## Transaction Cookie Configuration

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.

You can configure transaction cookies by providing a custom `TransactionStore` when initializing the Auth0 client:

```ts
import { TransactionStore } from "@auth0/nextjs-auth0/server";

const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
enableParallelTransactions: false, // Single transaction mode
cookieOptions: {
maxAge: 1800, // 30 minutes (in seconds)
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/"
}
});

export const auth0 = new Auth0Client({
transactionStore,
// ... other options
});
```

### Transaction Management Modes

**Parallel Transactions (Default)**
```ts
const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
enableParallelTransactions: true // Default: allows multiple concurrent logins
});
```

**Single Transaction Mode**
```ts
const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
enableParallelTransactions: false // Only one active transaction at a time
});
```

### Transaction Cookie Options

| Option | Type | Description |
| -------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| enableParallelTransactions | `boolean` | When `true` (default), allows multiple parallel login transactions for multi-tab support. When `false`, only one transaction cookie is maintained at a time. |
| 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. |
| 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_`. |
| cookieOptions.sameSite | `"strict" \| "lax" \| "none"` | Controls when the cookie is sent with cross-site requests. Defaults to `"lax"`. |
| 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. |
| cookieOptions.path | `string` | Specifies the URL path for which the cookie is valid. Defaults to `"/"`. |

### When to Use Single vs Parallel Transactions

**Use Parallel Transactions (Default) When:**
- Users might open multiple tabs and attempt to log in simultaneously
- You want maximum compatibility with typical user behavior
- Your application supports multiple concurrent authentication flows

**Use Single Transaction Mode When:**
- You want to prevent cookie accumulation issues in applications with frequent login attempts
- You prefer simpler transaction management
- Users typically don't need multiple concurrent login flows
- You're experiencing cookie header size limits due to abandoned transaction cookies

> [!NOTE]
> The SDK automatically cleans up transaction cookies after successful authentication or logout. The `maxAge` setting provides additional protection against abandoned cookies from incomplete authentication flows.

## Database sessions

By default, the user's sessions are stored in encrypted cookies. You may choose to persist the sessions in your data store of choice.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ import { Auth0Client } from "@auth0/nextjs-auth0/server";
export const auth0 = new Auth0Client();
```

> [!NOTE]
> The Auth0Client automatically creates a `TransactionStore` with safe defaults to manage authentication cookies. For advanced use cases, you can customize transaction cookie behavior by providing your own `TransactionStore` configuration. See [Transaction Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#transaction-cookie-configuration) for details.

### 4. Add the authentication middleware

Create a `middleware.ts` file in the root of your project's directory:
Expand Down Expand Up @@ -139,6 +142,7 @@ You can customize the client by using the options below:
| secret | `string` | A 32-byte, hex-encoded secret used for encrypting cookies. If it's not specified, it will be loaded from the `AUTH0_SECRET` environment variable. |
| signInReturnToPath | `string` | The path to redirect the user to after successfully authenticating. Defaults to `/`. |
| session | `SessionConfiguration` | Configure the session timeouts and whether to use rolling sessions or not. See [Session configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#session-configuration) for additional details. Also allows configuration of cookie attributes like `domain`, `path`, `secure`, `sameSite`, and `transient`. If not specified, these can be configured using `AUTH0_COOKIE_*` environment variables. Note: `httpOnly` is always `true`. See [Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#cookie-configuration) for details. |
| transactionStore | `TransactionStore` | Configure transaction cookie management for authentication flows. You can control parallel transaction support and cookie expiration. See [Transaction Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#transaction-cookie-configuration) for details. |
| beforeSessionSaved | `BeforeSessionSavedHook` | A method to manipulate the session before persisting it. See [beforeSessionSaved](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#beforesessionsaved) for additional details. |
| onCallback | `OnCallbackHook` | A method to handle errors or manage redirects after attempting to authenticate. See [onCallback](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#oncallback) for additional details. |
| sessionStore | `SessionStore` | A custom session store implementation used to persist sessions to a data store. See [Database sessions](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#database-sessions) for additional details. |
Expand Down
37 changes: 37 additions & 0 deletions V4_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,45 @@ export const auth0 = new Auth0Client({
- **Login parameters**: Use query parameters (`/auth/login?audience=...`) or static configuration
- **Session data**: Use the `beforeSessionSaved` hook to modify session data
- **Logout redirects**: Use query parameters (`/auth/logout?returnTo=...`)
- **Transaction cookies**: Configure transaction cookie behavior with `TransactionStore` options. See [Transaction Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#transaction-cookie-configuration) for details.

> [!IMPORTANT]
> Always validate redirect URLs to prevent open redirect attacks. Use relative URLs when possible.

For detailed examples and implementation patterns, see [Customizing Auth Handlers](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#customizing-auth-handlers) in the Examples guide.

## Transaction Cookie Management in V4

V4 introduces improved transaction cookie management to prevent cookie accumulation issues that could cause HTTP 413 errors. The `TransactionStore` now supports:

- **Configurable parallel transactions**: Control whether multiple login flows can run simultaneously
- **Automatic cookie cleanup**: Transaction cookies expire automatically after 1 hour by default
- **Customizable expiration**: Configure transaction cookie `maxAge` to suit your application needs

**Default Behavior (No Changes Required):**
```ts
// V4 automatically creates a TransactionStore with safe defaults
export const auth0 = new Auth0Client({
// All existing V3 options work the same way
});
```

**Custom Transaction Configuration:**
```ts
import { TransactionStore } from "@auth0/nextjs-auth0/server";

const transactionStore = new TransactionStore({
secret: process.env.AUTH0_SECRET!,
enableParallelTransactions: false, // Single-transaction mode
cookieOptions: {
maxAge: 1800 // 30 minutes instead of default 1 hour
}
});

export const auth0 = new Auth0Client({
transactionStore,
// ... other options
});
```

In contrast, V3 did not support parallel transactions.
78 changes: 45 additions & 33 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5039,14 +5039,17 @@ ca/T0LLtgmbMmxSv/MmzIg==

// Mock the transactionStore.save method to verify the saved state
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
expect(state.returnTo).toBe(defaultReturnTo);
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});
authClient["transactionStore"].save = vi.fn(
async (cookies, state, reqCookies) => {
expect(state.returnTo).toBe(defaultReturnTo);
return originalSave.call(
authClient["transactionStore"],
cookies,
state,
reqCookies
);
}
);

await authClient.startInteractiveLogin();

Expand All @@ -5059,14 +5062,17 @@ ca/T0LLtgmbMmxSv/MmzIg==

// Mock the transactionStore.save method to verify the saved state
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
expect(state.returnTo).toBe("/custom-return-path");
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});
authClient["transactionStore"].save = vi.fn(
async (cookies, state, reqCookies) => {
expect(state.returnTo).toBe("/custom-return-path");
return originalSave.call(
authClient["transactionStore"],
cookies,
state,
reqCookies
);
}
);

await authClient.startInteractiveLogin({ returnTo });

Expand All @@ -5079,14 +5085,17 @@ ca/T0LLtgmbMmxSv/MmzIg==
DEFAULT.appBaseUrl + "/custom-return-path?query=param#hash";

const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
expect(state.returnTo).toBe("/custom-return-path?query=param#hash");
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});
authClient["transactionStore"].save = vi.fn(
async (cookies, state, reqCookies) => {
expect(state.returnTo).toBe("/custom-return-path?query=param#hash");
return originalSave.call(
authClient["transactionStore"],
cookies,
state,
reqCookies
);
}
);

await authClient.startInteractiveLogin({ returnTo });

Expand All @@ -5101,15 +5110,18 @@ ca/T0LLtgmbMmxSv/MmzIg==

// Mock the transactionStore.save method to verify the saved state
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
// Should use the default safe path instead of the malicious one
expect(state.returnTo).toBe("/safe-path");
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});
authClient["transactionStore"].save = vi.fn(
async (cookies, state, reqCookies) => {
// Should use the default safe path instead of the malicious one
expect(state.returnTo).toBe("/safe-path");
return originalSave.call(
authClient["transactionStore"],
cookies,
state,
reqCookies
);
}
);

await authClient.startInteractiveLogin({ returnTo: unsafeReturnTo });

Expand Down
Loading