@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.
- package/README.md +276 -102
- package/dist/esm/actions.js +35 -4
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +51 -20
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +82 -93
- package/dist/esm/authkit-callback-route.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +36 -15
- package/dist/esm/components/authkit-provider.js.map +1 -1
- package/dist/esm/components/impersonation.js +17 -15
- package/dist/esm/components/impersonation.js.map +1 -1
- package/dist/esm/components/min-max-button.js +1 -1
- package/dist/esm/components/min-max-button.js.map +1 -1
- package/dist/esm/components/tokenStore.js +28 -19
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +1 -1
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/components/useTokenClaims.js +1 -1
- package/dist/esm/components/useTokenClaims.js.map +1 -1
- package/dist/esm/cookie.js +16 -5
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/env-variables.js +6 -6
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/errors.js +36 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +51 -12
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces.js +7 -1
- package/dist/esm/interfaces.js.map +1 -1
- package/dist/esm/middleware-helpers.js +102 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/pkce.js +38 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +73 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +1 -1
- package/dist/esm/test-helpers.js.map +1 -1
- package/dist/esm/types/actions.d.ts +34 -5
- package/dist/esm/types/auth.d.ts +7 -15
- package/dist/esm/types/components/authkit-provider.d.ts +6 -2
- package/dist/esm/types/components/impersonation.d.ts +2 -1
- package/dist/esm/types/cookie.d.ts +8 -0
- package/dist/esm/types/env-variables.d.ts +2 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/get-authorization-url.d.ts +2 -2
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/interfaces.d.ts +12 -0
- package/dist/esm/types/jwt.d.ts +9 -9
- package/dist/esm/types/middleware-helpers.d.ts +27 -0
- package/dist/esm/types/middleware.d.ts +3 -1
- package/dist/esm/types/pkce.d.ts +12 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/utils.d.ts +5 -0
- package/dist/esm/types/validate-api-key.d.ts +1 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +10 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/validate-api-key.js +16 -0
- package/dist/esm/validate-api-key.js.map +1 -0
- package/dist/esm/workos.js +1 -1
- package/package.json +32 -34
- package/src/actions.spec.ts +94 -17
- package/src/actions.ts +44 -5
- package/src/auth.spec.ts +60 -29
- package/src/auth.ts +55 -41
- package/src/authkit-callback-route.spec.ts +310 -58
- package/src/authkit-callback-route.ts +106 -103
- package/src/components/authkit-provider.spec.tsx +264 -70
- package/src/components/authkit-provider.tsx +40 -15
- package/src/components/button.spec.tsx +4 -6
- package/src/components/impersonation.spec.tsx +152 -35
- package/src/components/impersonation.tsx +37 -30
- package/src/components/min-max-button.spec.tsx +2 -1
- package/src/components/tokenStore.spec.ts +59 -44
- package/src/components/tokenStore.ts +11 -3
- package/src/components/useAccessToken.spec.tsx +82 -83
- package/src/components/useTokenClaims.spec.tsx +23 -22
- package/src/cookie.spec.ts +14 -9
- package/src/cookie.ts +29 -0
- package/src/env-variables.ts +2 -0
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.spec.ts +170 -15
- package/src/get-authorization-url.ts +69 -23
- package/src/index.ts +20 -2
- package/src/interfaces.ts +15 -0
- package/src/jwt.ts +9 -9
- package/src/middleware-helpers.spec.ts +238 -0
- package/src/middleware-helpers.ts +134 -0
- package/src/middleware.spec.ts +25 -0
- package/src/middleware.ts +4 -1
- package/src/pkce.spec.ts +125 -0
- package/src/pkce.ts +42 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +91 -27
- package/src/test-helpers.ts +1 -1
- package/src/utils.spec.ts +14 -31
- package/src/utils.ts +9 -0
- package/src/validate-api-key.spec.ts +111 -0
- package/src/validate-api-key.ts +19 -0
- package/src/workos.spec.ts +2 -2
- 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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
132
|
-
| ---------------------- |
|
|
133
|
-
| `user` | `User`
|
|
134
|
-
| `accessToken` | `string`
|
|
135
|
-
| `refreshToken` | `string`
|
|
136
|
-
| `impersonator` | `Impersonator \| undefined`
|
|
137
|
-
| `oauthTokens` | `OauthTokens \| undefined`
|
|
138
|
-
| `authenticationMethod` | `string \| undefined`
|
|
139
|
-
| `organizationId` | `string \| undefined`
|
|
140
|
-
| `state` | `
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
424
|
-
await addUserToTeam(user.id,
|
|
585
|
+
if (customData?.teamId) {
|
|
586
|
+
await addUserToTeam(user.id, customData.teamId);
|
|
425
587
|
}
|
|
426
588
|
|
|
427
|
-
if (
|
|
428
|
-
await trackFeatureActivation(user.id,
|
|
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:
|
|
435
|
-
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
|
|
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
|
-
|
|
535
|
-
|
|
698
|
+
async function handleClick() {
|
|
699
|
+
// Token is available immediately on initial page load
|
|
700
|
+
const token = await getAccessToken();
|
|
536
701
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 <
|
|
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
|
package/dist/esm/actions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
package/dist/esm/actions.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"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"}
|