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

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 +305 -102
  2. package/dist/esm/actions.js +35 -5
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +71 -21
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +90 -92
  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 +20 -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 +52 -0
  37. package/dist/esm/pkce.js.map +1 -0
  38. package/dist/esm/session.js +82 -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 +9 -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 +17 -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 +33 -34
  66. package/src/actions.spec.ts +91 -18
  67. package/src/actions.ts +44 -6
  68. package/src/auth.spec.ts +79 -29
  69. package/src/auth.ts +74 -42
  70. package/src/authkit-callback-route.spec.ts +372 -58
  71. package/src/authkit-callback-route.ts +121 -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 +63 -9
  83. package/src/cookie.ts +35 -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 +146 -0
  97. package/src/pkce.ts +59 -0
  98. package/src/session.spec.ts +87 -89
  99. package/src/session.ts +104 -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,81 @@ 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
+ ### Sign-in endpoint
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
+ Create a route that initiates the AuthKit sign-in flow. This route is used as the **[Sign-in endpoint](https://workos.com/docs/authkit/nextjs/2-configure-your-project/configure-a-redirect-uri#sign-in-endpoint)** (also known as `initiate_login_uri`) in your WorkOS dashboard settings.
147
148
 
148
149
  ```ts
150
+ // app/sign-in/route.ts (or app/login/route.ts)
151
+ import { getSignInUrl } from '@workos-inc/authkit-nextjs';
152
+ import { redirect } from 'next/navigation';
153
+
154
+ export const GET = async () => {
155
+ const signInUrl = await getSignInUrl();
156
+ return redirect(signInUrl);
157
+ };
158
+ ```
159
+
160
+ In the [WorkOS dashboard](https://dashboard.workos.com), go to **Redirects** and set the **Sign-in endpoint** to match this route (e.g., `http://localhost:3000/sign-in`).
161
+
162
+ > [!IMPORTANT]
163
+ > The sign-in endpoint is required for features like [impersonation](https://workos.com/docs/user-management/impersonation) to work correctly. Without it, WorkOS-initiated flows (such as impersonating a user from the dashboard) will fail because they cannot complete the PKCE/CSRF verification that this library enforces on every callback.
164
+
165
+ ### Proxy / Middleware
166
+
167
+ This library relies on Next.js proxy (called "middleware" in Next.js ≤15) to provide session management for routes.
168
+
169
+ **For Next.js 16+:** Create a `proxy.ts` file in the root of your project.
170
+ **For Next.js ≤15:** Create a `middleware.ts` file in the root of your project.
171
+
172
+ ```ts
173
+ // proxy.ts (Next.js 16+)
174
+ import { authkitProxy } from '@workos-inc/authkit-nextjs';
175
+
176
+ export default authkitProxy();
177
+
178
+ // Match against pages that require auth
179
+ export const config = { matcher: ['/', '/admin'] };
180
+ ```
181
+
182
+ ```ts
183
+ // middleware.ts (Next.js ≤15)
149
184
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
150
185
 
151
186
  export default authkitMiddleware();
152
187
 
153
188
  // Match against pages that require auth
154
- // Leave this out if you want auth on every resource (including images, css etc.)
155
189
  export const config = { matcher: ['/', '/admin'] };
156
190
  ```
157
191
 
158
- The middleware can be configured with several options.
192
+ > [!WARNING]
193
+ > 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:
194
+ >
195
+ > ```ts
196
+ > export const config = {
197
+ > matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
198
+ > };
199
+ > ```
200
+
201
+ The proxy/middleware can be configured with several options.
159
202
 
160
203
  | Option | Default | Description |
161
204
  | ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
162
205
  | `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. |
206
+ | `middlewareAuth` | `undefined` | Used to configure proxy/middleware auth options. See [middleware auth](#middleware-auth) for more details. |
164
207
  | `debug` | `false` | Enables debug logs. |
165
208
  | `signUpPaths` | `[]` | Used to specify paths that should use the 'sign-up' screen hint when redirecting to AuthKit. |
166
209
  | `eagerAuth` | `false` | Enables synchronous access token availability for third-party services. See [eager auth](#eager-auth) for more details. |
@@ -177,12 +220,123 @@ export default authkitMiddleware({
177
220
  });
178
221
 
179
222
  // Match against pages that require auth
180
- // Leave this out if you want auth on every resource (including images, css etc.)
181
223
  export const config = { matcher: ['/', '/admin'] };
182
224
  ```
183
225
 
184
226
  Custom redirect URIs will be used over a redirect URI configured in the environment variables.
185
227
 
228
+ #### Composable proxy/middleware
229
+
230
+ If you need to combine AuthKit with other proxy logic (rate limiting, redirects, etc.), use the `authkit()` function with `handleAuthkitHeaders()` helper:
231
+
232
+ ```ts
233
+ // proxy.ts (Next.js 16+) or middleware.ts (Next.js ≤15)
234
+ import { NextRequest } from 'next/server';
235
+ import { authkit, handleAuthkitHeaders } from '@workos-inc/authkit-nextjs';
236
+
237
+ export default async function proxy(request: NextRequest) {
238
+ // For Next.js ≤15, use: export default async function middleware(request: NextRequest) {
239
+ // Get session, headers, and the WorkOS authorization URL for sign-in redirects
240
+ const { session, headers, authorizationUrl } = await authkit(request);
241
+
242
+ const { pathname } = request.nextUrl;
243
+
244
+ // Redirect unauthenticated users on protected routes
245
+ if (pathname.startsWith('/app') && !session.user && authorizationUrl) {
246
+ return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl });
247
+ }
248
+
249
+ // Custom redirects (relative URLs supported)
250
+ if (pathname === '/old-path') {
251
+ return handleAuthkitHeaders(request, headers, { redirect: '/new-path' });
252
+ }
253
+
254
+ // Continue request with properly merged headers
255
+ return handleAuthkitHeaders(request, headers);
256
+ }
257
+
258
+ export const config = { matcher: ['/', '/app/:path*'] };
259
+ ```
260
+
261
+ > [!IMPORTANT]
262
+ > Always use `handleAuthkitHeaders()` when returning a response. This helper ensures:
263
+ >
264
+ > - AuthKit headers are properly passed to your pages (so `withAuth()` works)
265
+ > - Internal headers (session data, URLs) are never leaked to the browser
266
+ > - Only safe response headers (`Set-Cookie`, `Cache-Control`, `Vary`) are forwarded
267
+ > - `Cache-Control: no-store` is automatically set when cookies are present
268
+ > - `Vary` headers are properly merged when multiple values exist
269
+ > - Relative redirect URLs are automatically normalized to absolute URLs
270
+ > - POST/PUT redirects use 303 status to prevent form resubmission
271
+
272
+ > [!NOTE]
273
+ > 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.
274
+
275
+ ##### Redirect options
276
+
277
+ ```ts
278
+ handleAuthkitHeaders(request, headers, {
279
+ redirect: '/login', // URL to redirect to (string or URL object)
280
+ redirectStatus: 307, // 302 | 303 | 307 | 308 (default: 307 for GET, 303 for POST)
281
+ });
282
+ ```
283
+
284
+ ##### Advanced: Composing with rewrites
285
+
286
+ For advanced use cases like rewrites, use the lower-level `partitionAuthkitHeaders()` and `applyResponseHeaders()`:
287
+
288
+ ```ts
289
+ // proxy.ts (Next.js 16+) or middleware.ts (Next.js ≤15)
290
+ import { NextRequest, NextResponse } from 'next/server';
291
+ import { authkit, partitionAuthkitHeaders, applyResponseHeaders } from '@workos-inc/authkit-nextjs';
292
+
293
+ export default async function proxy(request: NextRequest) {
294
+ // For Next.js ≤15, use: export default async function middleware(request: NextRequest) {
295
+ const { headers } = await authkit(request);
296
+ const { requestHeaders, responseHeaders } = partitionAuthkitHeaders(request, headers);
297
+
298
+ // Create your own response (rewrite, etc.)
299
+ const response = NextResponse.rewrite(new URL('/app/dashboard', request.url), {
300
+ request: { headers: requestHeaders },
301
+ });
302
+
303
+ // Apply AuthKit response headers (Set-Cookie, etc.)
304
+ applyResponseHeaders(response, responseHeaders);
305
+
306
+ return response;
307
+ }
308
+ ```
309
+
310
+ ##### Internal headers reference
311
+
312
+ 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.
313
+
314
+ **Request headers** (passed to server components, never sent to browser):
315
+
316
+ | Header | Purpose |
317
+ | --------------------- | ------------------------------------------------------------------------------------------ |
318
+ | `x-workos-middleware` | Flag indicating AuthKit proxy/middleware is active. Required for `withAuth()` to function. |
319
+ | `x-workos-session` | Encrypted session data. Contains user info, access token, and refresh token. |
320
+ | `x-url` | Current request URL. Used for redirect-after-login and generating sign-in URLs. |
321
+ | `x-redirect-uri` | OAuth callback URI. Used by `getAuthorizationUrl()` for the OAuth flow. |
322
+ | `x-sign-up-paths` | Paths configured to trigger sign-up instead of sign-in flow. |
323
+
324
+ > **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.
325
+
326
+ **Response headers** (safe to send to browser):
327
+
328
+ | Header | Purpose |
329
+ | -------------------- | -------------------------------------------------------------------------------------- |
330
+ | `Set-Cookie` | Session cookies (e.g., `wos-session`). Multiple cookies are properly appended. |
331
+ | `Cache-Control` | Caching directives. Auto-set to `no-store` when cookies are present. |
332
+ | `Vary` | Cache variation keys. Values are deduplicated when merging. |
333
+ | `WWW-Authenticate` | Authentication challenge for 401 responses (API auth flows). |
334
+ | `Proxy-Authenticate` | Authentication challenge for proxy auth. |
335
+ | `Link` | Pagination, preload hints, etc. |
336
+ | `x-middleware-cache` | Next.js proxy/middleware result caching. Set to `no-cache` to prevent stale responses. |
337
+
338
+ Only these allowlisted headers are forwarded to the browser. Any other headers from `authkit()` (including future `x-workos-*` headers) are filtered out for security.
339
+
186
340
  ## Usage
187
341
 
188
342
  ### Wrap your app in `AuthKitProvider`
@@ -203,6 +357,31 @@ export default function RootLayout({ children }: { children: React.ReactNode })
203
357
  }
204
358
  ```
205
359
 
360
+ #### Optimizing with Server-Side Auth Data
361
+
362
+ To avoid a server action call on mount, you can pass the initial auth data from the server to the `AuthKitProvider`.
363
+
364
+ ```jsx
365
+ import { AuthKitProvider } from '@workos-inc/authkit-nextjs/components';
366
+ import { withAuth } from '@workos-inc/authkit-nextjs';
367
+
368
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
369
+ // Fetch auth data on the server
370
+ const auth = await withAuth();
371
+
372
+ // Remove the accessToken from the auth object as it is not needed on the client side
373
+ const { accessToken, ...initialAuth } = auth;
374
+
375
+ return (
376
+ <html lang="en">
377
+ <body>
378
+ <AuthKitProvider initialAuth={initialAuth}>{children}</AuthKitProvider>
379
+ </body>
380
+ </html>
381
+ );
382
+ }
383
+ ```
384
+
206
385
  ### Get the current user in a server component
207
386
 
208
387
  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 +403,10 @@ export default async function HomePage() {
224
403
 
225
404
  // You can also pass custom state data through the auth flow
226
405
  const signInUrlWithState = await getSignInUrl({
227
- state: {
406
+ state: JSON.stringify({
228
407
  teamId: 'team_123',
229
408
  referrer: 'homepage',
230
- },
409
+ }),
231
410
  });
232
411
 
233
412
  return (
@@ -403,41 +582,46 @@ JWT tokens are sensitive credentials and should be handled carefully:
403
582
 
404
583
  ### Passing Custom State Through Authentication
405
584
 
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:
585
+ 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
586
 
408
587
  ```ts
409
- // When generating sign-in/sign-up URLs
588
+ // When generating sign-in/sign-up URLs, serialize your data as JSON
410
589
  const signInUrl = await getSignInUrl({
411
- state: {
590
+ state: JSON.stringify({
412
591
  teamId: 'team_123',
413
592
  feature: 'billing',
414
593
  referrer: 'pricing-page',
415
594
  timestamp: Date.now(),
416
- },
595
+ }),
417
596
  });
418
597
 
419
598
  // The state data is available in the callback handler
420
599
  export const GET = handleAuth({
421
600
  onSuccess: async ({ user, state }) => {
601
+ // Parse the state string back to an object
602
+ const customData = state ? JSON.parse(state) : null;
603
+
422
604
  // Access your custom state data
423
- if (state?.teamId) {
424
- await addUserToTeam(user.id, state.teamId);
605
+ if (customData?.teamId) {
606
+ await addUserToTeam(user.id, customData.teamId);
425
607
  }
426
608
 
427
- if (state?.feature) {
428
- await trackFeatureActivation(user.id, state.feature);
609
+ if (customData?.feature) {
610
+ await trackFeatureActivation(user.id, customData.feature);
429
611
  }
430
612
 
431
613
  // Track where the user came from
432
614
  await analytics.track('sign_in_completed', {
433
615
  userId: user.id,
434
- referrer: state?.referrer,
435
- timestamp: state?.timestamp,
616
+ referrer: customData?.referrer,
617
+ timestamp: customData?.timestamp,
436
618
  });
437
619
  },
438
620
  });
439
621
  ```
440
622
 
623
+ > **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.
624
+
441
625
  This is useful for:
442
626
 
443
627
  - Tracking user journey and referral sources
@@ -467,16 +651,16 @@ const { session, headers } = await authkit(request, {
467
651
  });
468
652
  ```
469
653
 
470
- These callbacks provide a way to perform side effects when sessions are refreshed in the middleware. Common use cases include:
654
+ These callbacks provide a way to perform side effects when sessions are refreshed in the proxy/middleware. Common use cases include:
471
655
 
472
656
  - Logging authentication events
473
657
  - Updating last activity timestamps
474
658
  - Triggering organization-specific data prefetching
475
659
  - Recording failed refresh attempts
476
660
 
477
- ### Middleware auth
661
+ ### Proxy / Middleware auth
478
662
 
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:
663
+ 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
664
 
481
665
  ```ts
482
666
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
@@ -503,7 +687,7 @@ The `eagerAuth` option enables synchronous access to authentication tokens on in
503
687
 
504
688
  #### How it works
505
689
 
506
- When `eagerAuth: true` is set, the middleware temporarily stores the access token in a short-lived cookie (30 seconds) that is:
690
+ When `eagerAuth: true` is set, the proxy/middleware temporarily stores the access token in a short-lived cookie (30 seconds) that is:
507
691
 
508
692
  - Only set on initial page loads (not API or prefetch requests)
509
693
  - Immediately consumed and deleted by the client
@@ -511,7 +695,7 @@ When `eagerAuth: true` is set, the middleware temporarily stores the access toke
511
695
 
512
696
  #### Usage
513
697
 
514
- Enable eager auth in your middleware configuration:
698
+ Enable eager auth in your proxy/middleware configuration:
515
699
 
516
700
  ```ts
517
701
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
@@ -531,16 +715,18 @@ import { useAccessToken } from '@workos-inc/authkit-nextjs/components';
531
715
  function MyComponent() {
532
716
  const { getAccessToken } = useAccessToken();
533
717
 
534
- // Token is available immediately on initial page load
535
- const token = getAccessToken();
718
+ async function handleClick() {
719
+ // Token is available immediately on initial page load
720
+ const token = await getAccessToken();
536
721
 
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);
722
+ // Use with third-party services that need immediate token access
723
+ if (token) {
724
+ // Initialize your third-party client with the token
725
+ thirdPartyClient.authenticate(token);
726
+ }
541
727
  }
542
728
 
543
- return <div>...</div>;
729
+ return <button onClick={handleClick}>Authenticate</button>;
544
730
  }
545
731
  ```
546
732
 
@@ -565,64 +751,6 @@ Eager auth makes tokens briefly accessible via JavaScript (30-second window) to
565
751
  - Most API calls where a brief loading state is acceptable
566
752
  - When you don't need immediate token access on page load
567
753
 
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
754
  ### Signing out
627
755
 
628
756
  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".
@@ -638,6 +766,9 @@ await signOut({ returnTo: 'https://your-app.com/signed-out' });
638
766
  Render the `Impersonation` component in your app so that it is clear when someone is [impersonating a user](https://workos.com/docs/user-management/impersonation).
639
767
  The component will display a frame with some information about the impersonated user, as well as a button to stop impersonating.
640
768
 
769
+ > [!IMPORTANT]
770
+ > Impersonation requires a configured **Sign-in endpoint** in your WorkOS dashboard. See the [sign-in endpoint](#sign-in-endpoint) setup instructions. Without it, impersonation from the WorkOS dashboard will fail with a `Missing required auth parameter` error.
771
+
641
772
  ```jsx
642
773
  import { Impersonation, AuthKitProvider } from '@workos-inc/authkit-nextjs/components';
643
774
 
@@ -689,6 +820,25 @@ export default authkitMiddleware({
689
820
  });
690
821
  ```
691
822
 
823
+ ### Validate an API key
824
+
825
+ 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.
826
+
827
+ ```ts
828
+ import { NextResponse } from 'next/server';
829
+ import { validateApiKey } from '@workos-inc/authkit-nextjs';
830
+
831
+ export async function GET() {
832
+ const { apiKey } = await validateApiKey();
833
+
834
+ if (!apiKey) {
835
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
836
+ }
837
+
838
+ return NextResponse.json({ success: true });
839
+ }
840
+ ```
841
+
692
842
  ### Advanced: Direct access to the WorkOS client
693
843
 
694
844
  For advanced use cases or functionality not covered by the helper methods, you can access the underlying WorkOS client directly:
@@ -753,9 +903,35 @@ await saveSession(session, req);
753
903
  await saveSession(session, 'https://example.com/callback');
754
904
  ```
755
905
 
906
+ ### CDN Deployments and Caching
907
+
908
+ 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.
909
+
910
+ #### How It Works
911
+
912
+ The library automatically sets appropriate cache headers on all authenticated requests:
913
+
914
+ - `Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0` - Aggressive cache prevention with multiple directives
915
+ - `Pragma: no-cache` - HTTP/1.0 compatibility
916
+ - `Expires: 0` - HTTP/1.0 cache expiration
917
+ - `Vary: Cookie` - Ensures CDNs differentiate between different users (defense-in-depth)
918
+ - `x-middleware-cache: no-cache` - Prevents Next.js proxy/middleware result caching
919
+
920
+ These headers are applied automatically when:
921
+
922
+ - A session cookie is present in the request
923
+ - An Authorization header is detected
924
+ - An active authenticated session exists
925
+
926
+ #### Performance Considerations
927
+
928
+ **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.
929
+
930
+ **Public pages:** Unaffected by these security measures. Public routes without authentication context can still be cached normally.
931
+
756
932
  ### Debugging
757
933
 
758
- To enable debug logs, initialize the middleware with the debug flag enabled.
934
+ To enable debug logs, initialize the proxy/middleware with the debug flag enabled.
759
935
 
760
936
  ```js
761
937
  import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
@@ -763,8 +939,35 @@ import { authkitMiddleware } from '@workos-inc/authkit-nextjs';
763
939
  export default authkitMiddleware({ debug: true });
764
940
  ```
765
941
 
942
+ ### Security
943
+
944
+ #### PKCE and CSRF protection
945
+
946
+ 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.
947
+
948
+ > [!NOTE]
949
+ > **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.
950
+
951
+ #### Cookie requirements
952
+
953
+ 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.
954
+
955
+ If the cookie is missing or doesn't match, authentication will fail with one of:
956
+
957
+ - `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.
958
+ - `OAuth state mismatch` — the cookie and URL `state` parameter don't match, indicating a possible CSRF attack or cookie corruption.
959
+
960
+ > [!IMPORTANT]
961
+ > **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.
962
+
766
963
  ### Troubleshooting
767
964
 
965
+ #### `Missing required auth parameter` when impersonating from the WorkOS dashboard
966
+
967
+ This error occurs when WorkOS-initiated flows (like dashboard impersonation) redirect directly to your callback URL without going through your application's sign-in flow. Because this library enforces PKCE/CSRF verification on every callback, the request is rejected when the required `state` parameter is missing.
968
+
969
+ **Fix:** Configure a [sign-in endpoint](#sign-in-endpoint) in your WorkOS dashboard so that impersonation flows route through your app first, allowing PKCE/state to be set up before redirecting to WorkOS.
970
+
768
971
  #### NEXT_REDIRECT error when using try/catch blocks
769
972
 
770
973
  Wrapping a `withAuth({ ensureSignedIn: true })` call in a try/catch block will cause a `NEXT_REDIRECT` error. This is because `withAuth` will attempt to redirect the user to AuthKit if no session is detected and redirects in Next must be [called outside a try/catch](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#redirecting).
@@ -1,5 +1,5 @@
1
1
  'use server';
2
- import { signOut, switchToOrganization } from './auth.js';
2
+ import { getSignInUrl, signOut, switchToOrganization } from './auth.js';
3
3
  import { refreshSession, withAuth } from './session.js';
4
4
  import { getWorkOS } from './workos.js';
5
5
  /**
@@ -28,10 +28,28 @@ export const getOrganizationAction = async (organizationId) => {
28
28
  return await getWorkOS().organizations.getOrganization(organizationId);
29
29
  };
30
30
  export const getAuthAction = async (options) => {
31
- return sanitize(await withAuth(options));
31
+ // Never pass ensureSignedIn to withAuth from a server action, because withAuth
32
+ // would call redirect() to an external URL, which causes CORS errors when
33
+ // invoked via a client-side fetch. Instead, return the sign-in URL so the
34
+ // client can redirect via window.location.href.
35
+ const auth = await withAuth();
36
+ const sanitized = sanitize(auth);
37
+ if (options?.ensureSignedIn && !auth.user) {
38
+ const signInUrl = await getSignInUrl();
39
+ return { ...sanitized, signInUrl };
40
+ }
41
+ return sanitized;
32
42
  };
33
43
  export const refreshAuthAction = async ({ ensureSignedIn, organizationId, }) => {
34
- return sanitize(await refreshSession({ ensureSignedIn, organizationId }));
44
+ // Never pass ensureSignedIn to refreshSession from a server action for the
45
+ // same CORS reason as getAuthAction above.
46
+ const auth = await refreshSession({ organizationId });
47
+ const sanitized = sanitize(auth);
48
+ if (ensureSignedIn && !auth.user) {
49
+ const signInUrl = await getSignInUrl();
50
+ return { ...sanitized, signInUrl };
51
+ }
52
+ return sanitized;
35
53
  };
36
54
  export const switchToOrganizationAction = async (organizationId, options) => {
37
55
  return sanitize(await switchToOrganization(organizationId, options));
@@ -47,9 +65,21 @@ export async function getAccessTokenAction() {
47
65
  /**
48
66
  * This action is used to refresh the access token from the auth object.
49
67
  * It is used to fetch the access token from the server.
68
+ *
69
+ * Errors are caught and returned as data rather than thrown, to prevent
70
+ * Next.js from returning 500 responses for server action failures.
50
71
  */
51
72
  export async function refreshAccessTokenAction() {
52
- const auth = await refreshSession();
53
- return auth.accessToken;
73
+ try {
74
+ const auth = await refreshSession();
75
+ return { accessToken: auth.accessToken };
76
+ }
77
+ catch (error) {
78
+ console.warn('Failed to refresh access token:', error instanceof Error ? error.message : String(error));
79
+ return {
80
+ accessToken: undefined,
81
+ error: 'Failed to refresh access token',
82
+ };
83
+ }
54
84
  }
55
85
  //# 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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"}