@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +276 -102
  2. package/dist/esm/actions.js +35 -4
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +51 -20
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +82 -93
  7. package/dist/esm/authkit-callback-route.js.map +1 -1
  8. package/dist/esm/components/authkit-provider.js +36 -15
  9. package/dist/esm/components/authkit-provider.js.map +1 -1
  10. package/dist/esm/components/impersonation.js +17 -15
  11. package/dist/esm/components/impersonation.js.map +1 -1
  12. package/dist/esm/components/min-max-button.js +1 -1
  13. package/dist/esm/components/min-max-button.js.map +1 -1
  14. package/dist/esm/components/tokenStore.js +28 -19
  15. package/dist/esm/components/tokenStore.js.map +1 -1
  16. package/dist/esm/components/useAccessToken.js +1 -1
  17. package/dist/esm/components/useAccessToken.js.map +1 -1
  18. package/dist/esm/components/useTokenClaims.js +1 -1
  19. package/dist/esm/components/useTokenClaims.js.map +1 -1
  20. package/dist/esm/cookie.js +16 -5
  21. package/dist/esm/cookie.js.map +1 -1
  22. package/dist/esm/env-variables.js +6 -6
  23. package/dist/esm/env-variables.js.map +1 -1
  24. package/dist/esm/errors.js +36 -0
  25. package/dist/esm/errors.js.map +1 -0
  26. package/dist/esm/get-authorization-url.js +51 -12
  27. package/dist/esm/get-authorization-url.js.map +1 -1
  28. package/dist/esm/index.js +5 -2
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/interfaces.js +7 -1
  31. package/dist/esm/interfaces.js.map +1 -1
  32. package/dist/esm/middleware-helpers.js +102 -0
  33. package/dist/esm/middleware-helpers.js.map +1 -0
  34. package/dist/esm/middleware.js +3 -1
  35. package/dist/esm/middleware.js.map +1 -1
  36. package/dist/esm/pkce.js +38 -0
  37. package/dist/esm/pkce.js.map +1 -0
  38. package/dist/esm/session.js +73 -35
  39. package/dist/esm/session.js.map +1 -1
  40. package/dist/esm/test-helpers.js +1 -1
  41. package/dist/esm/test-helpers.js.map +1 -1
  42. package/dist/esm/types/actions.d.ts +34 -5
  43. package/dist/esm/types/auth.d.ts +7 -15
  44. package/dist/esm/types/components/authkit-provider.d.ts +6 -2
  45. package/dist/esm/types/components/impersonation.d.ts +2 -1
  46. package/dist/esm/types/cookie.d.ts +8 -0
  47. package/dist/esm/types/env-variables.d.ts +2 -1
  48. package/dist/esm/types/errors.d.ts +15 -0
  49. package/dist/esm/types/get-authorization-url.d.ts +2 -2
  50. package/dist/esm/types/index.d.ts +5 -2
  51. package/dist/esm/types/interfaces.d.ts +12 -0
  52. package/dist/esm/types/jwt.d.ts +9 -9
  53. package/dist/esm/types/middleware-helpers.d.ts +27 -0
  54. package/dist/esm/types/middleware.d.ts +3 -1
  55. package/dist/esm/types/pkce.d.ts +12 -0
  56. package/dist/esm/types/session.d.ts +1 -1
  57. package/dist/esm/types/utils.d.ts +5 -0
  58. package/dist/esm/types/validate-api-key.d.ts +1 -0
  59. package/dist/esm/types/workos.d.ts +1 -1
  60. package/dist/esm/utils.js +10 -2
  61. package/dist/esm/utils.js.map +1 -1
  62. package/dist/esm/validate-api-key.js +16 -0
  63. package/dist/esm/validate-api-key.js.map +1 -0
  64. package/dist/esm/workos.js +1 -1
  65. package/package.json +32 -34
  66. package/src/actions.spec.ts +94 -17
  67. package/src/actions.ts +44 -5
  68. package/src/auth.spec.ts +60 -29
  69. package/src/auth.ts +55 -41
  70. package/src/authkit-callback-route.spec.ts +310 -58
  71. package/src/authkit-callback-route.ts +106 -103
  72. package/src/components/authkit-provider.spec.tsx +264 -70
  73. package/src/components/authkit-provider.tsx +40 -15
  74. package/src/components/button.spec.tsx +4 -6
  75. package/src/components/impersonation.spec.tsx +152 -35
  76. package/src/components/impersonation.tsx +37 -30
  77. package/src/components/min-max-button.spec.tsx +2 -1
  78. package/src/components/tokenStore.spec.ts +59 -44
  79. package/src/components/tokenStore.ts +11 -3
  80. package/src/components/useAccessToken.spec.tsx +82 -83
  81. package/src/components/useTokenClaims.spec.tsx +23 -22
  82. package/src/cookie.spec.ts +14 -9
  83. package/src/cookie.ts +29 -0
  84. package/src/env-variables.ts +2 -0
  85. package/src/errors.spec.ts +108 -0
  86. package/src/errors.ts +46 -0
  87. package/src/get-authorization-url.spec.ts +170 -15
  88. package/src/get-authorization-url.ts +69 -23
  89. package/src/index.ts +20 -2
  90. package/src/interfaces.ts +15 -0
  91. package/src/jwt.ts +9 -9
  92. package/src/middleware-helpers.spec.ts +238 -0
  93. package/src/middleware-helpers.ts +134 -0
  94. package/src/middleware.spec.ts +25 -0
  95. package/src/middleware.ts +4 -1
  96. package/src/pkce.spec.ts +125 -0
  97. package/src/pkce.ts +42 -0
  98. package/src/session.spec.ts +87 -89
  99. package/src/session.ts +91 -27
  100. package/src/test-helpers.ts +1 -1
  101. package/src/utils.spec.ts +14 -31
  102. package/src/utils.ts +9 -0
  103. package/src/validate-api-key.spec.ts +111 -0
  104. package/src/validate-api-key.ts +19 -0
  105. package/src/workos.spec.ts +2 -2
  106. package/src/workos.ts +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@ The AuthKit library for Next.js provides convenient helpers for authentication a
9
9
  Install the package with:
10
10
 
11
11
  ```
12
- npm i @workos-inc/authkit-nextjs
12
+ pnpm i @workos-inc/authkit-nextjs
13
13
  ```
14
14
 
15
15
  or
@@ -100,8 +100,9 @@ export const GET = handleAuth({
100
100
  await saveAuthMethod(user.id, authenticationMethod);
101
101
  }
102
102
  // Access custom state data passed through the auth flow
103
- if (state?.teamId) {
104
- await addUserToTeam(user.id, state.teamId);
103
+ const customData = state ? JSON.parse(state) : null;
104
+ if (customData?.teamId) {
105
+ await addUserToTeam(user.id, customData.teamId);
105
106
  }
106
107
  },
107
108
  });
@@ -128,39 +129,61 @@ export const GET = handleAuth({
128
129
 
129
130
  The `onSuccess` callback receives the following data:
130
131
 
131
- | Property | Type | Description |
132
- | ---------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------- |
133
- | `user` | `User` | The authenticated user object |
134
- | `accessToken` | `string` | JWT access token |
135
- | `refreshToken` | `string` | Refresh token for session renewal |
136
- | `impersonator` | `Impersonator \| undefined` | Present if user is being impersonated |
137
- | `oauthTokens` | `OauthTokens \| undefined` | OAuth tokens from upstream provider |
138
- | `authenticationMethod` | `string \| undefined` | How the user authenticated (e.g., 'password', 'google-oauth'). Only available during initial login |
139
- | `organizationId` | `string \| undefined` | Organization context of authentication |
140
- | `state` | `Record<string, any> \| undefined` | Custom state data passed through the authentication flow |
132
+ | Property | Type | Description |
133
+ | ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
134
+ | `user` | `User` | The authenticated user object |
135
+ | `accessToken` | `string` | JWT access token |
136
+ | `refreshToken` | `string` | Refresh token for session renewal |
137
+ | `impersonator` | `Impersonator \| undefined` | Present if user is being impersonated |
138
+ | `oauthTokens` | `OauthTokens \| undefined` | OAuth tokens from upstream provider |
139
+ | `authenticationMethod` | `string \| undefined` | How the user authenticated (e.g., 'password', 'google-oauth'). Only available during initial login |
140
+ | `organizationId` | `string \| undefined` | Organization context of authentication |
141
+ | `state` | `string \| undefined` | Custom state string passed through the authentication flow (parse with JSON.parse if needed) |
141
142
 
142
143
  **Note**: `authenticationMethod` is only provided during the initial authentication callback. It will not be available in subsequent requests or session refreshes.
143
144
 
144
- ### Middleware
145
+ ### Proxy / Middleware
145
146
 
146
- This library relies on [Next.js middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) to provide session management for routes. Put the following in your `middleware.ts` file in the root of your project:
147
+ This library relies on Next.js proxy (called "middleware" in Next.js ≤15) to provide session management for routes.
148
+
149
+ **For Next.js 16+:** Create a `proxy.ts` file in the root of your project.
150
+ **For Next.js ≤15:** Create a `middleware.ts` file in the root of your project.
151
+
152
+ ```ts
153
+ // proxy.ts (Next.js 16+)
154
+ import { authkitProxy } from '@workos-inc/authkit-nextjs';
155
+
156
+ export default authkitProxy();
157
+
158
+ // Match against pages that require auth
159
+ export const config = { matcher: ['/', '/admin'] };
160
+ ```
147
161
 
148
162
  ```ts
163
+ // middleware.ts (Next.js ≤15)
149
164
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
150
165
 
151
166
  export default authkitMiddleware();
152
167
 
153
168
  // Match against pages that require auth
154
- // Leave this out if you want auth on every resource (including images, css etc.)
155
169
  export const config = { matcher: ['/', '/admin'] };
156
170
  ```
157
171
 
158
- The middleware can be configured with several options.
172
+ > [!WARNING]
173
+ > Using a catch-all matcher pattern can intercept static assets (CSS, images, fonts), causing styles to break—particularly with Tailwind CSS v4. If you need a broad matcher, exclude Next.js static paths:
174
+ >
175
+ > ```ts
176
+ > export const config = {
177
+ > matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
178
+ > };
179
+ > ```
180
+
181
+ The proxy/middleware can be configured with several options.
159
182
 
160
183
  | Option | Default | Description |
161
184
  | ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
162
185
  | `redirectUri` | `undefined` | Used in cases where you need your redirect URI to be set dynamically (e.g. Vercel preview deployments) |
163
- | `middlewareAuth` | `undefined` | Used to configure middleware auth options. See [middleware auth](#middleware-auth) for more details. |
186
+ | `middlewareAuth` | `undefined` | Used to configure proxy/middleware auth options. See [middleware auth](#middleware-auth) for more details. |
164
187
  | `debug` | `false` | Enables debug logs. |
165
188
  | `signUpPaths` | `[]` | Used to specify paths that should use the 'sign-up' screen hint when redirecting to AuthKit. |
166
189
  | `eagerAuth` | `false` | Enables synchronous access token availability for third-party services. See [eager auth](#eager-auth) for more details. |
@@ -177,12 +200,123 @@ export default authkitMiddleware({
177
200
  });
178
201
 
179
202
  // Match against pages that require auth
180
- // Leave this out if you want auth on every resource (including images, css etc.)
181
203
  export const config = { matcher: ['/', '/admin'] };
182
204
  ```
183
205
 
184
206
  Custom redirect URIs will be used over a redirect URI configured in the environment variables.
185
207
 
208
+ #### Composable proxy/middleware
209
+
210
+ If you need to combine AuthKit with other proxy logic (rate limiting, redirects, etc.), use the `authkit()` function with `handleAuthkitHeaders()` helper:
211
+
212
+ ```ts
213
+ // proxy.ts (Next.js 16+) or middleware.ts (Next.js ≤15)
214
+ import { NextRequest } from 'next/server';
215
+ import { authkit, handleAuthkitHeaders } from '@workos-inc/authkit-nextjs';
216
+
217
+ export default async function proxy(request: NextRequest) {
218
+ // For Next.js ≤15, use: export default async function middleware(request: NextRequest) {
219
+ // Get session, headers, and the WorkOS authorization URL for sign-in redirects
220
+ const { session, headers, authorizationUrl } = await authkit(request);
221
+
222
+ const { pathname } = request.nextUrl;
223
+
224
+ // Redirect unauthenticated users on protected routes
225
+ if (pathname.startsWith('/app') && !session.user && authorizationUrl) {
226
+ return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl });
227
+ }
228
+
229
+ // Custom redirects (relative URLs supported)
230
+ if (pathname === '/old-path') {
231
+ return handleAuthkitHeaders(request, headers, { redirect: '/new-path' });
232
+ }
233
+
234
+ // Continue request with properly merged headers
235
+ return handleAuthkitHeaders(request, headers);
236
+ }
237
+
238
+ export const config = { matcher: ['/', '/app/:path*'] };
239
+ ```
240
+
241
+ > [!IMPORTANT]
242
+ > Always use `handleAuthkitHeaders()` when returning a response. This helper ensures:
243
+ >
244
+ > - AuthKit headers are properly passed to your pages (so `withAuth()` works)
245
+ > - Internal headers (session data, URLs) are never leaked to the browser
246
+ > - Only safe response headers (`Set-Cookie`, `Cache-Control`, `Vary`) are forwarded
247
+ > - `Cache-Control: no-store` is automatically set when cookies are present
248
+ > - `Vary` headers are properly merged when multiple values exist
249
+ > - Relative redirect URLs are automatically normalized to absolute URLs
250
+ > - POST/PUT redirects use 303 status to prevent form resubmission
251
+
252
+ > [!NOTE]
253
+ > The `redirect` option should only be used with trusted values (e.g., `authorizationUrl` from `authkit()` or hardcoded paths). Never pass user-controlled input directly to `redirect` without validation, as this could enable open redirect attacks.
254
+
255
+ ##### Redirect options
256
+
257
+ ```ts
258
+ handleAuthkitHeaders(request, headers, {
259
+ redirect: '/login', // URL to redirect to (string or URL object)
260
+ redirectStatus: 307, // 302 | 303 | 307 | 308 (default: 307 for GET, 303 for POST)
261
+ });
262
+ ```
263
+
264
+ ##### Advanced: Composing with rewrites
265
+
266
+ For advanced use cases like rewrites, use the lower-level `partitionAuthkitHeaders()` and `applyResponseHeaders()`:
267
+
268
+ ```ts
269
+ // proxy.ts (Next.js 16+) or middleware.ts (Next.js ≤15)
270
+ import { NextRequest, NextResponse } from 'next/server';
271
+ import { authkit, partitionAuthkitHeaders, applyResponseHeaders } from '@workos-inc/authkit-nextjs';
272
+
273
+ export default async function proxy(request: NextRequest) {
274
+ // For Next.js ≤15, use: export default async function middleware(request: NextRequest) {
275
+ const { headers } = await authkit(request);
276
+ const { requestHeaders, responseHeaders } = partitionAuthkitHeaders(request, headers);
277
+
278
+ // Create your own response (rewrite, etc.)
279
+ const response = NextResponse.rewrite(new URL('/app/dashboard', request.url), {
280
+ request: { headers: requestHeaders },
281
+ });
282
+
283
+ // Apply AuthKit response headers (Set-Cookie, etc.)
284
+ applyResponseHeaders(response, responseHeaders);
285
+
286
+ return response;
287
+ }
288
+ ```
289
+
290
+ ##### Internal headers reference
291
+
292
+ AuthKit uses internal headers to pass data between proxy/middleware and server components. These are automatically handled by the helpers above, but understanding them helps with debugging.
293
+
294
+ **Request headers** (passed to server components, never sent to browser):
295
+
296
+ | Header | Purpose |
297
+ | --------------------- | ------------------------------------------------------------------------------------------ |
298
+ | `x-workos-middleware` | Flag indicating AuthKit proxy/middleware is active. Required for `withAuth()` to function. |
299
+ | `x-workos-session` | Encrypted session data. Contains user info, access token, and refresh token. |
300
+ | `x-url` | Current request URL. Used for redirect-after-login and generating sign-in URLs. |
301
+ | `x-redirect-uri` | OAuth callback URI. Used by `getAuthorizationUrl()` for the OAuth flow. |
302
+ | `x-sign-up-paths` | Paths configured to trigger sign-up instead of sign-in flow. |
303
+
304
+ > **Security:** These headers contain sensitive session data. The `handleAuthkitHeaders()` helper ensures they're forwarded to your pages (so `withAuth()` works) but never leaked to the browser. Client-injected `x-workos-*` headers are stripped and replaced with trusted values.
305
+
306
+ **Response headers** (safe to send to browser):
307
+
308
+ | Header | Purpose |
309
+ | -------------------- | -------------------------------------------------------------------------------------- |
310
+ | `Set-Cookie` | Session cookies (e.g., `wos-session`). Multiple cookies are properly appended. |
311
+ | `Cache-Control` | Caching directives. Auto-set to `no-store` when cookies are present. |
312
+ | `Vary` | Cache variation keys. Values are deduplicated when merging. |
313
+ | `WWW-Authenticate` | Authentication challenge for 401 responses (API auth flows). |
314
+ | `Proxy-Authenticate` | Authentication challenge for proxy auth. |
315
+ | `Link` | Pagination, preload hints, etc. |
316
+ | `x-middleware-cache` | Next.js proxy/middleware result caching. Set to `no-cache` to prevent stale responses. |
317
+
318
+ Only these allowlisted headers are forwarded to the browser. Any other headers from `authkit()` (including future `x-workos-*` headers) are filtered out for security.
319
+
186
320
  ## Usage
187
321
 
188
322
  ### Wrap your app in `AuthKitProvider`
@@ -203,6 +337,31 @@ export default function RootLayout({ children }: { children: React.ReactNode })
203
337
  }
204
338
  ```
205
339
 
340
+ #### Optimizing with Server-Side Auth Data
341
+
342
+ To avoid a server action call on mount, you can pass the initial auth data from the server to the `AuthKitProvider`.
343
+
344
+ ```jsx
345
+ import { AuthKitProvider } from '@workos-inc/authkit-nextjs/components';
346
+ import { withAuth } from '@workos-inc/authkit-nextjs';
347
+
348
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
349
+ // Fetch auth data on the server
350
+ const auth = await withAuth();
351
+
352
+ // Remove the accessToken from the auth object as it is not needed on the client side
353
+ const { accessToken, ...initialAuth } = auth;
354
+
355
+ return (
356
+ <html lang="en">
357
+ <body>
358
+ <AuthKitProvider initialAuth={initialAuth}>{children}</AuthKitProvider>
359
+ </body>
360
+ </html>
361
+ );
362
+ }
363
+ ```
364
+
206
365
  ### Get the current user in a server component
207
366
 
208
367
  For pages where you want to display a signed-in and signed-out view, use `withAuth` to retrieve the user session from WorkOS.
@@ -224,10 +383,10 @@ export default async function HomePage() {
224
383
 
225
384
  // You can also pass custom state data through the auth flow
226
385
  const signInUrlWithState = await getSignInUrl({
227
- state: {
386
+ state: JSON.stringify({
228
387
  teamId: 'team_123',
229
388
  referrer: 'homepage',
230
- },
389
+ }),
231
390
  });
232
391
 
233
392
  return (
@@ -403,41 +562,46 @@ JWT tokens are sensitive credentials and should be handled carefully:
403
562
 
404
563
  ### Passing Custom State Through Authentication
405
564
 
406
- You can pass custom state data through the authentication flow using the `state` parameter. This data will be available in the `onSuccess` callback after authentication:
565
+ You can pass custom state data through the authentication flow using the `state` parameter. The state parameter is a string value that gets passed through OAuth and returned in the callback. To pass complex data, serialize it as JSON:
407
566
 
408
567
  ```ts
409
- // When generating sign-in/sign-up URLs
568
+ // When generating sign-in/sign-up URLs, serialize your data as JSON
410
569
  const signInUrl = await getSignInUrl({
411
- state: {
570
+ state: JSON.stringify({
412
571
  teamId: 'team_123',
413
572
  feature: 'billing',
414
573
  referrer: 'pricing-page',
415
574
  timestamp: Date.now(),
416
- },
575
+ }),
417
576
  });
418
577
 
419
578
  // The state data is available in the callback handler
420
579
  export const GET = handleAuth({
421
580
  onSuccess: async ({ user, state }) => {
581
+ // Parse the state string back to an object
582
+ const customData = state ? JSON.parse(state) : null;
583
+
422
584
  // Access your custom state data
423
- if (state?.teamId) {
424
- await addUserToTeam(user.id, state.teamId);
585
+ if (customData?.teamId) {
586
+ await addUserToTeam(user.id, customData.teamId);
425
587
  }
426
588
 
427
- if (state?.feature) {
428
- await trackFeatureActivation(user.id, state.feature);
589
+ if (customData?.feature) {
590
+ await trackFeatureActivation(user.id, customData.feature);
429
591
  }
430
592
 
431
593
  // Track where the user came from
432
594
  await analytics.track('sign_in_completed', {
433
595
  userId: user.id,
434
- referrer: state?.referrer,
435
- timestamp: state?.timestamp,
596
+ referrer: customData?.referrer,
597
+ timestamp: customData?.timestamp,
436
598
  });
437
599
  },
438
600
  });
439
601
  ```
440
602
 
603
+ > **Note**: The `state` parameter is an opaque string as defined by OAuth 2.0 (RFC 6749). If you need to pass structured data, you must serialize it yourself using `JSON.stringify()` and parse it with `JSON.parse()` in the callback.
604
+
441
605
  This is useful for:
442
606
 
443
607
  - Tracking user journey and referral sources
@@ -467,16 +631,16 @@ const { session, headers } = await authkit(request, {
467
631
  });
468
632
  ```
469
633
 
470
- These callbacks provide a way to perform side effects when sessions are refreshed in the middleware. Common use cases include:
634
+ These callbacks provide a way to perform side effects when sessions are refreshed in the proxy/middleware. Common use cases include:
471
635
 
472
636
  - Logging authentication events
473
637
  - Updating last activity timestamps
474
638
  - Triggering organization-specific data prefetching
475
639
  - Recording failed refresh attempts
476
640
 
477
- ### Middleware auth
641
+ ### Proxy / Middleware auth
478
642
 
479
- The default behavior of this library is to request authentication via the `withAuth` method on a per-page basis. There are some use cases where you don't want to call `withAuth` (e.g. you don't need user data for your page) or if you'd prefer a "secure by default" approach where every route defined in your middleware matcher is protected unless specified otherwise. In those cases you can opt-in to use middleware auth instead:
643
+ The default behavior of this library is to request authentication via the `withAuth` method on a per-page basis. There are some use cases where you don't want to call `withAuth` (e.g. you don't need user data for your page) or if you'd prefer a "secure by default" approach where every route defined in your proxy/middleware matcher is protected unless specified otherwise. In those cases you can opt-in to use `middlewareAuth` instead:
480
644
 
481
645
  ```ts
482
646
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
@@ -503,7 +667,7 @@ The `eagerAuth` option enables synchronous access to authentication tokens on in
503
667
 
504
668
  #### How it works
505
669
 
506
- When `eagerAuth: true` is set, the middleware temporarily stores the access token in a short-lived cookie (30 seconds) that is:
670
+ When `eagerAuth: true` is set, the proxy/middleware temporarily stores the access token in a short-lived cookie (30 seconds) that is:
507
671
 
508
672
  - Only set on initial page loads (not API or prefetch requests)
509
673
  - Immediately consumed and deleted by the client
@@ -511,7 +675,7 @@ When `eagerAuth: true` is set, the middleware temporarily stores the access toke
511
675
 
512
676
  #### Usage
513
677
 
514
- Enable eager auth in your middleware configuration:
678
+ Enable eager auth in your proxy/middleware configuration:
515
679
 
516
680
  ```ts
517
681
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
@@ -531,16 +695,18 @@ import { useAccessToken } from '@workos-inc/authkit-nextjs/components';
531
695
  function MyComponent() {
532
696
  const { getAccessToken } = useAccessToken();
533
697
 
534
- // Token is available immediately on initial page load
535
- const token = getAccessToken();
698
+ async function handleClick() {
699
+ // Token is available immediately on initial page load
700
+ const token = await getAccessToken();
536
701
 
537
- // Use with third-party services that need immediate token access
538
- if (token) {
539
- // Initialize your third-party client with the token
540
- thirdPartyClient.authenticate(token);
702
+ // Use with third-party services that need immediate token access
703
+ if (token) {
704
+ // Initialize your third-party client with the token
705
+ thirdPartyClient.authenticate(token);
706
+ }
541
707
  }
542
708
 
543
- return <div>...</div>;
709
+ return <button onClick={handleClick}>Authenticate</button>;
544
710
  }
545
711
  ```
546
712
 
@@ -565,64 +731,6 @@ Eager auth makes tokens briefly accessible via JavaScript (30-second window) to
565
731
  - Most API calls where a brief loading state is acceptable
566
732
  - When you don't need immediate token access on page load
567
733
 
568
- ### Composing middleware
569
-
570
- > **Security note:** Always forward `request.headers` when returning `NextResponse.*` to mitigate SSRF issues in Next.js < 14.2.32 (14.x) or < 15.4.7 (15.x). This pattern is safe on all versions. We strongly recommend upgrading to the latest Next.js.
571
-
572
- If you don't want to use `authkitMiddleware` and instead want to compose your own middleware, you can use the `authkit` method. In this mode you are responsible to handling what to do when there's no session on a protected route.
573
-
574
- ```ts
575
- export default async function middleware(request: NextRequest) {
576
- // Perform logic before or after AuthKit
577
-
578
- // Auth object contains the session, response headers and an authorization URL in the case that the session isn't valid
579
- // This method will automatically handle setting the cookie and refreshing the session
580
- const {
581
- session,
582
- headers: authkitHeaders,
583
- authorizationUrl,
584
- } = await authkit(request, {
585
- debug: true,
586
- });
587
-
588
- const { pathname } = new URL(request.url);
589
-
590
- // Control of what to do when there's no session on a protected route is left to the developer
591
- if (pathname.startsWith('/account') && !session.user) {
592
- console.log('No session on protected path');
593
-
594
- // Preserve AuthKit headers on redirects (e.g., cookies)
595
- const response = NextResponse.redirect(authorizationUrl);
596
- for (const [key, value] of authkitHeaders) {
597
- if (key.toLowerCase() === 'set-cookie') {
598
- response.headers.append(key, value);
599
- } else {
600
- response.headers.set(key, value);
601
- }
602
- }
603
- return response;
604
- }
605
-
606
- // Forward the incoming request headers (mitigation) and then add AuthKit's headers
607
- const response = NextResponse.next({
608
- request: { headers: new Headers(request.headers) },
609
- });
610
-
611
- for (const [key, value] of authkitHeaders) {
612
- if (key.toLowerCase() === 'set-cookie') {
613
- response.headers.append(key, value);
614
- } else {
615
- response.headers.set(key, value);
616
- }
617
- }
618
-
619
- return response;
620
- }
621
-
622
- // Match against the pages
623
- export const config = { matcher: ['/', '/account/:path*'] };
624
- ```
625
-
626
734
  ### Signing out
627
735
 
628
736
  Use the `signOut` method to sign out the current logged in user and redirect to your app's default Logout URI. The Logout URI is set in your WorkOS dashboard settings under "Redirect".
@@ -689,6 +797,25 @@ export default authkitMiddleware({
689
797
  });
690
798
  ```
691
799
 
800
+ ### Validate an API key
801
+
802
+ 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.
803
+
804
+ ```ts
805
+ import { NextResponse } from 'next/server';
806
+ import { validateApiKey } from '@workos-inc/authkit-nextjs';
807
+
808
+ export async function GET() {
809
+ const { apiKey } = await validateApiKey();
810
+
811
+ if (!apiKey) {
812
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
813
+ }
814
+
815
+ return NextResponse.json({ success: true });
816
+ }
817
+ ```
818
+
692
819
  ### Advanced: Direct access to the WorkOS client
693
820
 
694
821
  For advanced use cases or functionality not covered by the helper methods, you can access the underlying WorkOS client directly:
@@ -753,9 +880,35 @@ await saveSession(session, req);
753
880
  await saveSession(session, 'https://example.com/callback');
754
881
  ```
755
882
 
883
+ ### CDN Deployments and Caching
884
+
885
+ 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.
886
+
887
+ #### How It Works
888
+
889
+ The library automatically sets appropriate cache headers on all authenticated requests:
890
+
891
+ - `Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0` - Aggressive cache prevention with multiple directives
892
+ - `Pragma: no-cache` - HTTP/1.0 compatibility
893
+ - `Expires: 0` - HTTP/1.0 cache expiration
894
+ - `Vary: Cookie` - Ensures CDNs differentiate between different users (defense-in-depth)
895
+ - `x-middleware-cache: no-cache` - Prevents Next.js proxy/middleware result caching
896
+
897
+ These headers are applied automatically when:
898
+
899
+ - A session cookie is present in the request
900
+ - An Authorization header is detected
901
+ - An active authenticated session exists
902
+
903
+ #### Performance Considerations
904
+
905
+ **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.
906
+
907
+ **Public pages:** Unaffected by these security measures. Public routes without authentication context can still be cached normally.
908
+
756
909
  ### Debugging
757
910
 
758
- To enable debug logs, initialize the middleware with the debug flag enabled.
911
+ To enable debug logs, initialize the proxy/middleware with the debug flag enabled.
759
912
 
760
913
  ```js
761
914
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
@@ -763,6 +916,27 @@ import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
763
916
  export default authkitMiddleware({ debug: true });
764
917
  ```
765
918
 
919
+ ### Security
920
+
921
+ #### PKCE and CSRF protection
922
+
923
+ This library uses [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange) and a sealed (encrypted) OAuth state parameter on every authorization request. The state contains a cryptographic nonce for CSRF protection per [RFC 9700](https://datatracker.ietf.org/doc/rfc9700/) and a code verifier for protection against authorization code interception. During sign-in, a short-lived `wos-auth-verifier` cookie is set containing the sealed state. This cookie is automatically cleaned up after the callback completes.
924
+
925
+ > [!NOTE]
926
+ > **Upgrading to v3:** PKCE is now always enabled. The `WORKOS_ENABLE_PKCE` environment variable is no longer needed and can be removed from your configuration.
927
+
928
+ #### Cookie requirements
929
+
930
+ The `wos-auth-verifier` cookie must survive the round-trip from sign-in initiation to the callback. On callback, the library verifies that the cookie is present and matches the URL `state` parameter — this two-channel check is what prevents CSRF attacks.
931
+
932
+ If the cookie is missing or doesn't match, authentication will fail with one of:
933
+
934
+ - `Auth cookie missing` — the cookie was not sent back with the callback request. This typically happens when a reverse proxy or CDN strips `Set-Cookie` headers on redirects.
935
+ - `OAuth state mismatch` — the cookie and URL `state` parameter don't match, indicating a possible CSRF attack or cookie corruption.
936
+
937
+ > [!IMPORTANT]
938
+ > **Upgrading to v3:** Previous versions would silently fall back to verifying only the URL `state` parameter when the cookie was missing. This fallback has been removed because it disabled CSRF protection. If you see `Auth cookie missing` errors after upgrading, ensure that `Set-Cookie` headers are propagated on redirects between your application and the user's browser.
939
+
766
940
  ### Troubleshooting
767
941
 
768
942
  #### NEXT_REDIRECT error when using try/catch blocks
@@ -1,6 +1,7 @@
1
1
  'use server';
2
2
  import { signOut, switchToOrganization } from './auth.js';
3
3
  import { refreshSession, withAuth } from './session.js';
4
+ import { getAuthorizationUrl } from './get-authorization-url.js';
4
5
  import { getWorkOS } from './workos.js';
5
6
  /**
6
7
  * This function is used to sanitize the auth object.
@@ -28,10 +29,28 @@ export const getOrganizationAction = async (organizationId) => {
28
29
  return await getWorkOS().organizations.getOrganization(organizationId);
29
30
  };
30
31
  export const getAuthAction = async (options) => {
31
- return sanitize(await withAuth(options));
32
+ // Never pass ensureSignedIn to withAuth from a server action, because withAuth
33
+ // would call redirect() to an external URL, which causes CORS errors when
34
+ // invoked via a client-side fetch. Instead, return the sign-in URL so the
35
+ // client can redirect via window.location.href.
36
+ const auth = await withAuth();
37
+ const sanitized = sanitize(auth);
38
+ if (options?.ensureSignedIn && !auth.user) {
39
+ const signInUrl = await getAuthorizationUrl({ screenHint: 'sign-in' });
40
+ return { ...sanitized, signInUrl };
41
+ }
42
+ return sanitized;
32
43
  };
33
44
  export const refreshAuthAction = async ({ ensureSignedIn, organizationId, }) => {
34
- return sanitize(await refreshSession({ ensureSignedIn, organizationId }));
45
+ // Never pass ensureSignedIn to refreshSession from a server action for the
46
+ // same CORS reason as getAuthAction above.
47
+ const auth = await refreshSession({ organizationId });
48
+ const sanitized = sanitize(auth);
49
+ if (ensureSignedIn && !auth.user) {
50
+ const signInUrl = await getAuthorizationUrl({ screenHint: 'sign-in' });
51
+ return { ...sanitized, signInUrl };
52
+ }
53
+ return sanitized;
35
54
  };
36
55
  export const switchToOrganizationAction = async (organizationId, options) => {
37
56
  return sanitize(await switchToOrganization(organizationId, options));
@@ -47,9 +66,21 @@ export async function getAccessTokenAction() {
47
66
  /**
48
67
  * This action is used to refresh the access token from the auth object.
49
68
  * It is used to fetch the access token from the server.
69
+ *
70
+ * Errors are caught and returned as data rather than thrown, to prevent
71
+ * Next.js from returning 500 responses for server action failures.
50
72
  */
51
73
  export async function refreshAccessTokenAction() {
52
- const auth = await refreshSession();
53
- return auth.accessToken;
74
+ try {
75
+ const auth = await refreshSession();
76
+ return { accessToken: auth.accessToken };
77
+ }
78
+ catch (error) {
79
+ console.warn('Failed to refresh access token:', error instanceof Error ? error.message : String(error));
80
+ return {
81
+ accessToken: undefined,
82
+ error: 'Failed to refresh access token',
83
+ };
84
+ }
54
85
  }
55
86
  //# sourceMappingURL=actions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;GAKG;AACH,SAAS,QAAQ,CAAkC,KAAQ;IACzD,6DAA6D;IAC7D,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE,GAAG,KAAK,CAAC;IAC5C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,IAAI,EAAE;IAC3C,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,EAAE,QAAQ,KAA4B,EAAE,EAAE,EAAE;IACpF,MAAM,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC9B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,cAAsB,EAAE,EAAE;IACpE,OAAO,MAAM,SAAS,EAAE,CAAC,aAAa,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;AACzE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,OAAsC,EAAE,EAAE;IAC5E,OAAO,QAAQ,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,EACtC,cAAc,EACd,cAAc,GAIf,EAAE,EAAE;IACH,OAAO,QAAQ,CAAC,MAAM,cAAc,CAAC,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;AAC5E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,EAAE,cAAsB,EAAE,OAAqC,EAAE,EAAE;IAChH,OAAO,QAAQ,CAAC,MAAM,oBAAoB,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,IAAI,GAAG,MAAM,QAAQ,EAAE,CAAC;IAC9B,OAAO,IAAI,CAAC,WAAW,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,MAAM,IAAI,GAAG,MAAM,cAAc,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,WAAW,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"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"}