Skip to content
Closed
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
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,17 +707,17 @@ export default authkitMiddleware({
Use the `validateApiKey` function in your application's public API endpoints to parse a [Bearer Authentication](https://swagger.io/docs/specification/v3_0/authentication/bearer-authentication/) header and validate the [API key](https://workos.com/docs/authkit/api-keys) with WorkOS.

```ts
import { NextResponse } from 'next/server'
import { validateApiKey } from '@workos-inc/authkit-nextjs'
import { NextResponse } from 'next/server';
import { validateApiKey } from '@workos-inc/authkit-nextjs';

export async function GET() {
const { apiKey } = await validateApiKey()
const { apiKey } = await validateApiKey();

if (!apiKey) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

return NextResponse.json({ success: true })
return NextResponse.json({ success: true });
}
```

Expand Down Expand Up @@ -785,6 +785,31 @@ await saveSession(session, req);
await saveSession(session, 'https://example.com/callback');
```

### CDN Deployments and Caching

AuthKit automatically implements cache security measures to protect against session leakage in CDN environments. This is particularly important when deploying to AWS with SST/OpenNext, Cloudflare, or other CDN configurations.

#### How It Works

The library automatically sets appropriate cache headers on all authenticated requests:

- `Cache-Control: private, no-store, must-revalidate` - Prevents CDN caching of authenticated responses
- `Vary: Cookie` - Ensures CDNs differentiate between different users (defense-in-depth)
- `x-middleware-cache: no-cache` - Prevents Next.js middleware result caching
- `Pragma: no-cache` - HTTP/1.0 compatibility

These headers are applied automatically when:

- A session cookie is present in the request
- An Authorization header is detected
- An active authenticated session exists

#### Performance Considerations

**Authenticated pages:** Will not be cached at the CDN level and will always hit your origin server. This is the correct and secure behavior for session-based authentication.

**Public pages:** Unaffected by these security measures. Public routes without authentication context can still be cached normally.

### Debugging

To enable debug logs, initialize the middleware with the debug flag enabled.
Expand Down
26 changes: 21 additions & 5 deletions src/authkit-callback-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import { saveSession } from './session.js';
import { errorResponseWithFallback, redirectWithFallback } from './utils.js';
import { getWorkOS } from './workos.js';

/**
* Prevents CDN caching of auth callback responses (critical for security)
*/
function preventCaching(headers: Headers): void {
headers.set('Vary', 'Cookie');
headers.set('Cache-Control', 'private, no-store, must-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('x-middleware-cache', 'no-cache');
}

function handleState(state: string | null) {
let returnPathname: string | undefined = undefined;
let userState: string | undefined;
Expand Down Expand Up @@ -90,6 +100,7 @@ export function handleAuth(options: HandleAuthOptions = {}) {
// Fall back to standard Response if NextResponse is not available.
// This is to support Next.js 13.
const response = redirectWithFallback(url.toString());
preventCaching(response.headers);

if (!accessToken || !refreshToken) throw new Error('response is missing tokens');

Expand All @@ -116,23 +127,28 @@ export function handleAuth(options: HandleAuthOptions = {}) {

console.error(errorRes);

return errorResponse(request, error);
return await errorResponse(request, error);
}
}

return errorResponse(request);
return await errorResponse(request);
};

function errorResponse(request: NextRequest, error?: unknown) {
async function errorResponse(request: NextRequest, error?: unknown) {
if (onError) {
return onError({ error, request });
const response = await onError({ error, request });
preventCaching(response.headers);
return response;
}

return errorResponseWithFallback({
const response = errorResponseWithFallback({
error: {
message: 'Something went wrong',
description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.",
},
});

preventCaching(response.headers);
return response;
}
}
33 changes: 33 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ const jwtCookieName = 'workos-access-token';

const JWKS = lazy(() => createRemoteJWKSet(new URL(getWorkOS().userManagement.getJwksUrl(WORKOS_CLIENT_ID))));

/**
* Applies cache security headers to prevent CDN caching of authenticated responses.
* Critical for preventing session crossover in CDN environments (CloudFront, Cloudflare, etc.)
*/
function applyCacheSecurityHeaders(headers: Headers, request: NextRequest): void {
// Build Vary header with deduplication
const varyValues = ['Cookie'];
if (request.headers.has('authorization')) {
varyValues.push('Authorization');
}

const currentVary = headers.get('Vary');
if (currentVary) {
const existing = new Set(currentVary.split(',').map((v) => v.trim()));
varyValues.forEach((v) => existing.add(v));
headers.set('Vary', Array.from(existing).join(', '));
} else {
headers.set('Vary', varyValues.join(', '));
}

// Prevent caching - critical for CDN security
headers.set('Cache-Control', 'private, no-store, must-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('x-middleware-cache', 'no-cache');
}

/**
* Determines if a request is for an initial document load (not API/RSC/prefetch)
*/
Expand Down Expand Up @@ -120,6 +146,13 @@ async function updateSessionMiddleware(
headers.set(signUpPathsHeaderName, signUpPaths.join(','));
}

// Apply cache security headers to prevent CDN caching of authenticated responses
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';

if (session?.accessToken != null || request.cookies.has(cookieName) || request.headers.has('authorization')) {
applyCacheSecurityHeaders(headers, request);
}

return NextResponse.next({
headers,
});
Expand Down