@workos-inc/authkit-nextjs 2.17.0 → 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 +40 -11
- package/dist/esm/actions.js +35 -4
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +13 -22
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +71 -95
- package/dist/esm/authkit-callback-route.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +31 -13
- package/dist/esm/components/authkit-provider.js.map +1 -1
- package/dist/esm/components/impersonation.js +9 -9
- 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 +5 -7
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/errors.js +7 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/get-authorization-url.js +23 -27
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +3 -3
- 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 +8 -5
- package/dist/esm/middleware-helpers.js.map +1 -1
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/pkce.js +17 -22
- package/dist/esm/pkce.js.map +1 -1
- package/dist/esm/session.js +19 -23
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/types/actions.d.ts +34 -5
- package/dist/esm/types/auth.d.ts +6 -16
- package/dist/esm/types/cookie.d.ts +8 -0
- package/dist/esm/types/env-variables.d.ts +1 -2
- package/dist/esm/types/get-authorization-url.d.ts +1 -1
- package/dist/esm/types/index.d.ts +3 -3
- package/dist/esm/types/interfaces.d.ts +9 -1
- package/dist/esm/types/jwt.d.ts +9 -9
- package/dist/esm/types/middleware-helpers.d.ts +3 -1
- package/dist/esm/types/middleware.d.ts +3 -1
- package/dist/esm/types/pkce.d.ts +6 -5
- package/dist/esm/utils.js +2 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/validate-api-key.js +1 -2
- package/dist/esm/validate-api-key.js.map +1 -1
- package/package.json +12 -13
- package/src/actions.spec.ts +81 -6
- package/src/actions.ts +44 -5
- package/src/auth.spec.ts +3 -2
- package/src/auth.ts +12 -43
- package/src/authkit-callback-route.spec.ts +210 -60
- package/src/authkit-callback-route.ts +94 -107
- package/src/components/authkit-provider.spec.tsx +89 -6
- package/src/components/authkit-provider.tsx +20 -1
- package/src/components/impersonation.spec.tsx +1 -0
- package/src/components/impersonation.tsx +29 -24
- package/src/components/tokenStore.spec.ts +35 -20
- package/src/components/tokenStore.ts +11 -3
- package/src/components/useAccessToken.spec.tsx +15 -12
- package/src/components/useTokenClaims.spec.tsx +1 -0
- package/src/cookie.ts +29 -0
- package/src/env-variables.ts +0 -2
- package/src/get-authorization-url.spec.ts +18 -40
- package/src/get-authorization-url.ts +34 -40
- package/src/index.ts +3 -1
- package/src/interfaces.ts +11 -1
- package/src/jwt.ts +9 -9
- package/src/middleware-helpers.spec.ts +7 -0
- package/src/middleware-helpers.ts +7 -3
- package/src/middleware.spec.ts +25 -0
- package/src/middleware.ts +4 -1
- package/src/pkce.spec.ts +125 -0
- package/src/pkce.ts +19 -19
- package/src/session.spec.ts +18 -22
- package/src/session.ts +10 -12
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Mock } from 'vitest';
|
|
1
2
|
import { getWorkOS } from './workos.js';
|
|
2
3
|
import { handleAuth } from './authkit-callback-route.js';
|
|
3
4
|
import { getSessionFromCookie, saveSession } from './session.js';
|
|
@@ -6,6 +7,7 @@ import { sealData } from 'iron-session';
|
|
|
6
7
|
|
|
7
8
|
// Mocked in vitest.setup.ts
|
|
8
9
|
import { cookies, headers } from 'next/headers';
|
|
10
|
+
import { State } from './interfaces.js';
|
|
9
11
|
|
|
10
12
|
// Mock dependencies
|
|
11
13
|
const { fakeWorkosInstance } = vi.hoisted(() => ({
|
|
@@ -52,6 +54,12 @@ describe('authkit-callback-route', () => {
|
|
|
52
54
|
},
|
|
53
55
|
};
|
|
54
56
|
|
|
57
|
+
async function setAuthCookie(req: NextRequest, state: State): Promise<string> {
|
|
58
|
+
const sealedState = await sealData(state, { password: process.env.WORKOS_COOKIE_PASSWORD! });
|
|
59
|
+
req.cookies.set('wos-auth-verifier', sealedState);
|
|
60
|
+
return sealedState;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
describe('handleAuth', () => {
|
|
56
64
|
let request: NextRequest;
|
|
57
65
|
|
|
@@ -80,8 +88,10 @@ describe('authkit-callback-route', () => {
|
|
|
80
88
|
it('should handle successful authentication', async () => {
|
|
81
89
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
82
90
|
|
|
83
|
-
// Set up request with code
|
|
91
|
+
// Set up request with code & state
|
|
92
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
84
93
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
94
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
85
95
|
|
|
86
96
|
const handler = handleAuth();
|
|
87
97
|
const response = await handler(request);
|
|
@@ -89,15 +99,17 @@ describe('authkit-callback-route', () => {
|
|
|
89
99
|
expect(workos.userManagement.authenticateWithCode).toHaveBeenCalledWith({
|
|
90
100
|
clientId: process.env.WORKOS_CLIENT_ID,
|
|
91
101
|
code: 'test-code',
|
|
102
|
+
codeVerifier: 'test-verifier',
|
|
92
103
|
});
|
|
93
104
|
expect(response).toBeInstanceOf(NextResponse);
|
|
94
105
|
});
|
|
95
106
|
|
|
96
107
|
it('should handle authentication failure', async () => {
|
|
97
|
-
// Mock authentication failure
|
|
98
108
|
(workos.userManagement.authenticateWithCode as Mock).mockRejectedValue(new Error('Auth failed'));
|
|
99
109
|
|
|
110
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
100
111
|
request.nextUrl.searchParams.set('code', 'invalid-code');
|
|
112
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
101
113
|
|
|
102
114
|
const handler = handleAuth();
|
|
103
115
|
const response = await handler(request);
|
|
@@ -108,10 +120,11 @@ describe('authkit-callback-route', () => {
|
|
|
108
120
|
});
|
|
109
121
|
|
|
110
122
|
it('should handle authentication failure if a non-Error object is thrown', async () => {
|
|
111
|
-
// Mock authentication failure
|
|
112
123
|
vi.mocked(workos.userManagement.authenticateWithCode).mockRejectedValue('Auth failed');
|
|
113
124
|
|
|
125
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
114
126
|
request.nextUrl.searchParams.set('code', 'invalid-code');
|
|
127
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
115
128
|
|
|
116
129
|
const handler = handleAuth();
|
|
117
130
|
const response = await handler(request);
|
|
@@ -122,9 +135,11 @@ describe('authkit-callback-route', () => {
|
|
|
122
135
|
});
|
|
123
136
|
|
|
124
137
|
it('should handle authentication failure with custom onError handler', async () => {
|
|
125
|
-
// Mock authentication failure
|
|
126
138
|
vi.mocked(workos.userManagement.authenticateWithCode).mockRejectedValue('Auth failed');
|
|
139
|
+
|
|
140
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
127
141
|
request.nextUrl.searchParams.set('code', 'invalid-code');
|
|
142
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
128
143
|
|
|
129
144
|
const handler = handleAuth({
|
|
130
145
|
onError: () => {
|
|
@@ -153,7 +168,9 @@ describe('authkit-callback-route', () => {
|
|
|
153
168
|
it('should respect custom returnPathname', async () => {
|
|
154
169
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
155
170
|
|
|
171
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
156
172
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
173
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
157
174
|
|
|
158
175
|
const handler = handleAuth({ returnPathname: '/dashboard' });
|
|
159
176
|
const response = await handler(request);
|
|
@@ -164,9 +181,13 @@ describe('authkit-callback-route', () => {
|
|
|
164
181
|
it('should handle state parameter with returnPathname', async () => {
|
|
165
182
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
166
183
|
|
|
167
|
-
const
|
|
184
|
+
const sealedState = await setAuthCookie(request, {
|
|
185
|
+
nonce: 'foo',
|
|
186
|
+
codeVerifier: 'test-verifier',
|
|
187
|
+
returnPathname: '/custom-path',
|
|
188
|
+
});
|
|
168
189
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
169
|
-
request.nextUrl.searchParams.set('state',
|
|
190
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
170
191
|
|
|
171
192
|
const handler = handleAuth();
|
|
172
193
|
const response = await handler(request);
|
|
@@ -177,9 +198,13 @@ describe('authkit-callback-route', () => {
|
|
|
177
198
|
it('should extract custom search params from returnPathname', async () => {
|
|
178
199
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
179
200
|
|
|
180
|
-
const
|
|
201
|
+
const sealedState = await setAuthCookie(request, {
|
|
202
|
+
nonce: 'foo',
|
|
203
|
+
codeVerifier: 'test-verifier',
|
|
204
|
+
returnPathname: '/custom-path?foo=bar&baz=qux',
|
|
205
|
+
});
|
|
181
206
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
182
|
-
request.nextUrl.searchParams.set('state',
|
|
207
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
183
208
|
|
|
184
209
|
const handler = handleAuth();
|
|
185
210
|
const response = await handler(request);
|
|
@@ -190,9 +215,14 @@ describe('authkit-callback-route', () => {
|
|
|
190
215
|
it('should handle full URL in returnPathname by extracting only the pathname', async () => {
|
|
191
216
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
192
217
|
|
|
193
|
-
const
|
|
218
|
+
const sealedState = await setAuthCookie(request, {
|
|
219
|
+
nonce: 'foo',
|
|
220
|
+
codeVerifier: 'test-verifier',
|
|
221
|
+
returnPathname: 'https://example.com/invite/k0123456789',
|
|
222
|
+
});
|
|
223
|
+
|
|
194
224
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
195
|
-
request.nextUrl.searchParams.set('state',
|
|
225
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
196
226
|
|
|
197
227
|
const handler = handleAuth();
|
|
198
228
|
const response = await handler(request);
|
|
@@ -208,8 +238,9 @@ describe('authkit-callback-route', () => {
|
|
|
208
238
|
|
|
209
239
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
210
240
|
|
|
211
|
-
|
|
241
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
212
242
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
243
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
213
244
|
|
|
214
245
|
const handler = handleAuth();
|
|
215
246
|
const response = await handler(request);
|
|
@@ -224,6 +255,10 @@ describe('authkit-callback-route', () => {
|
|
|
224
255
|
const originalJson = NextResponse.json;
|
|
225
256
|
(NextResponse as Partial<typeof NextResponse>).json = undefined;
|
|
226
257
|
|
|
258
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
259
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
260
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
261
|
+
|
|
227
262
|
const handler = handleAuth();
|
|
228
263
|
const response = await handler(request);
|
|
229
264
|
|
|
@@ -240,8 +275,10 @@ describe('authkit-callback-route', () => {
|
|
|
240
275
|
it('should use baseURL if provided', async () => {
|
|
241
276
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
242
277
|
|
|
243
|
-
// Set up request with code
|
|
278
|
+
// Set up request with code & state
|
|
279
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
244
280
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
281
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
245
282
|
|
|
246
283
|
const handler = handleAuth({ baseURL: 'https://base.com' });
|
|
247
284
|
const response = await handler(request);
|
|
@@ -250,14 +287,15 @@ describe('authkit-callback-route', () => {
|
|
|
250
287
|
});
|
|
251
288
|
|
|
252
289
|
it('should throw an error if response is missing tokens', async () => {
|
|
253
|
-
const
|
|
290
|
+
const incompleteAuthResponse = {
|
|
254
291
|
user: { id: 'user_123' },
|
|
255
292
|
};
|
|
256
293
|
|
|
257
|
-
(workos.userManagement.authenticateWithCode as Mock).mockResolvedValue(
|
|
294
|
+
(workos.userManagement.authenticateWithCode as Mock).mockResolvedValue(incompleteAuthResponse);
|
|
258
295
|
|
|
259
|
-
|
|
296
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
260
297
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
298
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
261
299
|
|
|
262
300
|
const handler = handleAuth();
|
|
263
301
|
const response = await handler(request);
|
|
@@ -268,8 +306,10 @@ describe('authkit-callback-route', () => {
|
|
|
268
306
|
it('should call onSuccess if provided', async () => {
|
|
269
307
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
270
308
|
|
|
271
|
-
// Set up request with code
|
|
309
|
+
// Set up request with code & state
|
|
310
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
272
311
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
312
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
273
313
|
|
|
274
314
|
const onSuccess = vi.fn();
|
|
275
315
|
const handler = handleAuth({ onSuccess: onSuccess });
|
|
@@ -284,8 +324,9 @@ describe('authkit-callback-route', () => {
|
|
|
284
324
|
const newAccessToken = 'new-access-token';
|
|
285
325
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
286
326
|
|
|
287
|
-
|
|
327
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
288
328
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
329
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
289
330
|
|
|
290
331
|
const handler = handleAuth({
|
|
291
332
|
onSuccess: async (data) => {
|
|
@@ -301,15 +342,15 @@ describe('authkit-callback-route', () => {
|
|
|
301
342
|
it('should pass custom state data to onSuccess callback', async () => {
|
|
302
343
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
303
344
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
345
|
+
const sealedState = await setAuthCookie(request, {
|
|
346
|
+
nonce: 'foo',
|
|
347
|
+
codeVerifier: 'test-verifier',
|
|
348
|
+
returnPathname: '/dashboard',
|
|
349
|
+
customState: 'custom-user-state-string',
|
|
350
|
+
});
|
|
310
351
|
|
|
311
352
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
312
|
-
request.nextUrl.searchParams.set('state',
|
|
353
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
313
354
|
|
|
314
355
|
const onSuccess = vi.fn();
|
|
315
356
|
const handler = handleAuth({ onSuccess });
|
|
@@ -332,10 +373,14 @@ describe('authkit-callback-route', () => {
|
|
|
332
373
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
333
374
|
|
|
334
375
|
// State with only returnPathname
|
|
335
|
-
const
|
|
376
|
+
const sealedState = await setAuthCookie(request, {
|
|
377
|
+
nonce: 'foo',
|
|
378
|
+
codeVerifier: 'test-verifier',
|
|
379
|
+
returnPathname: '/profile',
|
|
380
|
+
});
|
|
336
381
|
|
|
337
382
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
338
|
-
request.nextUrl.searchParams.set('state',
|
|
383
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
339
384
|
|
|
340
385
|
const onSuccess = vi.fn();
|
|
341
386
|
const handler = handleAuth({ onSuccess });
|
|
@@ -350,51 +395,129 @@ describe('authkit-callback-route', () => {
|
|
|
350
395
|
);
|
|
351
396
|
});
|
|
352
397
|
|
|
353
|
-
it('should handle backward compatibility with old state format', async () => {
|
|
398
|
+
it('should NOT handle backward compatibility with old state format', async () => {
|
|
354
399
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
355
400
|
|
|
356
401
|
// Old format: just returnPathname
|
|
357
|
-
|
|
358
|
-
|
|
402
|
+
// @ts-expect-error we're purposely testing backward compatibility with an old format that doesn't match the current State interface
|
|
403
|
+
const sealedState = await setAuthCookie(request, { returnPathname: '/old-path' });
|
|
359
404
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
360
|
-
request.nextUrl.searchParams.set('state',
|
|
405
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
361
406
|
|
|
362
407
|
const handler = handleAuth();
|
|
363
408
|
const response = await handler(request);
|
|
364
409
|
|
|
365
|
-
// Should
|
|
366
|
-
expect(response.
|
|
410
|
+
// Should error
|
|
411
|
+
expect(response.status).toBe(500);
|
|
412
|
+
expect(workos.userManagement.authenticateWithCode).not.toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should not leak nonce-only state as custom state in onSuccess', async () => {
|
|
416
|
+
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
417
|
+
|
|
418
|
+
// Simulate a nonce-only state (no returnPathname, no custom state)
|
|
419
|
+
const nonceState = await setAuthCookie(request, { nonce: 'test-nonce', codeVerifier: 'test-verifier' });
|
|
420
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
421
|
+
request.nextUrl.searchParams.set('state', nonceState);
|
|
422
|
+
|
|
423
|
+
const onSuccess = vi.fn();
|
|
424
|
+
const handler = handleAuth({ onSuccess });
|
|
425
|
+
await handler(request);
|
|
426
|
+
|
|
427
|
+
expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({ state: undefined }));
|
|
367
428
|
});
|
|
368
429
|
|
|
369
|
-
describe('
|
|
370
|
-
it('should
|
|
430
|
+
describe('state verification', () => {
|
|
431
|
+
it('should reject callback when state does not match stored state', async () => {
|
|
371
432
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
372
433
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
434
|
+
const state = 'attacker-state';
|
|
435
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
436
|
+
request.nextUrl.searchParams.set('state', state);
|
|
437
|
+
await setAuthCookie(request, { nonce: 'legitimate-state', codeVerifier: 'test-verifier' });
|
|
438
|
+
|
|
439
|
+
const handler = handleAuth();
|
|
440
|
+
const response = await handler(request);
|
|
441
|
+
|
|
442
|
+
expect(response.status).toBe(500);
|
|
443
|
+
expect(workos.userManagement.authenticateWithCode).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should reject when state is present but no cookie exists', async () => {
|
|
447
|
+
const sealedState = await sealData(
|
|
448
|
+
{ nonce: 'foo', codeVerifier: 'test-verifier' },
|
|
376
449
|
{ password: process.env.WORKOS_COOKIE_PASSWORD! },
|
|
377
450
|
);
|
|
451
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
452
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
453
|
+
|
|
454
|
+
const handler = handleAuth();
|
|
455
|
+
const response = await handler(request);
|
|
378
456
|
|
|
379
|
-
|
|
380
|
-
|
|
457
|
+
expect(workos.userManagement.authenticateWithCode).not.toHaveBeenCalled();
|
|
458
|
+
expect(response.status).toBe(500);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should pass when state matches stored state', async () => {
|
|
462
|
+
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
463
|
+
|
|
464
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier' });
|
|
381
465
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
466
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
382
467
|
|
|
383
468
|
const handler = handleAuth();
|
|
384
|
-
await handler(request);
|
|
469
|
+
const response = await handler(request);
|
|
470
|
+
|
|
471
|
+
expect(workos.userManagement.authenticateWithCode).toHaveBeenCalled();
|
|
472
|
+
expect(response.status).not.toBe(500);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should return 500 when neither state nor cookie exist', async () => {
|
|
476
|
+
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
477
|
+
|
|
478
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
479
|
+
|
|
480
|
+
const handler = handleAuth();
|
|
481
|
+
const response = await handler(request);
|
|
482
|
+
|
|
483
|
+
expect(workos.userManagement.authenticateWithCode).not.toHaveBeenCalled();
|
|
484
|
+
expect(response.status).toBe(500);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe('PKCE', () => {
|
|
489
|
+
it('should pass codeVerifier and verify state when both are in the cookie', async () => {
|
|
490
|
+
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
491
|
+
|
|
492
|
+
const sealedState = await setAuthCookie(request, {
|
|
493
|
+
codeVerifier: 'test-verifier-456',
|
|
494
|
+
returnPathname: '/dashboard',
|
|
495
|
+
nonce: 'foo',
|
|
496
|
+
});
|
|
497
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
498
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
499
|
+
|
|
500
|
+
const handler = handleAuth();
|
|
501
|
+
const response = await handler(request);
|
|
385
502
|
|
|
386
503
|
expect(workos.userManagement.authenticateWithCode).toHaveBeenCalledWith(
|
|
387
504
|
expect.objectContaining({
|
|
388
505
|
code: 'test-code',
|
|
389
|
-
codeVerifier: 'test-verifier-
|
|
506
|
+
codeVerifier: 'test-verifier-456',
|
|
390
507
|
}),
|
|
391
508
|
);
|
|
509
|
+
expect(response.headers.get('Location')).toContain('/dashboard');
|
|
392
510
|
});
|
|
393
511
|
|
|
394
|
-
it('should
|
|
512
|
+
it('should pass codeVerifier from cookie to authenticateWithCode', async () => {
|
|
395
513
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
514
|
+
const sealedState = await setAuthCookie(request, {
|
|
515
|
+
nonce: 'foo',
|
|
516
|
+
codeVerifier: 'test-verifier-123',
|
|
517
|
+
});
|
|
396
518
|
|
|
397
519
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
520
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
398
521
|
|
|
399
522
|
const handler = handleAuth();
|
|
400
523
|
await handler(request);
|
|
@@ -402,46 +525,73 @@ describe('authkit-callback-route', () => {
|
|
|
402
525
|
expect(workos.userManagement.authenticateWithCode).toHaveBeenCalledWith(
|
|
403
526
|
expect.objectContaining({
|
|
404
527
|
code: 'test-code',
|
|
405
|
-
codeVerifier:
|
|
528
|
+
codeVerifier: 'test-verifier-123',
|
|
406
529
|
}),
|
|
407
530
|
);
|
|
408
531
|
});
|
|
409
532
|
|
|
410
|
-
it('should
|
|
533
|
+
it('should reject when cookie is missing even if state contains valid sealed data', async () => {
|
|
534
|
+
const sealedState = await sealData(
|
|
535
|
+
{ nonce: 'foo', codeVerifier: 'test-verifier-123' },
|
|
536
|
+
{ password: process.env.WORKOS_COOKIE_PASSWORD! },
|
|
537
|
+
);
|
|
538
|
+
request.nextUrl.searchParams.set('code', 'test-code');
|
|
539
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
540
|
+
|
|
541
|
+
const handler = handleAuth();
|
|
542
|
+
const response = await handler(request);
|
|
543
|
+
|
|
544
|
+
expect(workos.userManagement.authenticateWithCode).not.toHaveBeenCalled();
|
|
545
|
+
expect(response.status).toBe(500);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should return an error response when PKCE cookie is corrupted', async () => {
|
|
411
549
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
412
550
|
|
|
413
551
|
// Set a corrupted cookie
|
|
414
|
-
request.cookies.set('wos-
|
|
552
|
+
request.cookies.set('wos-auth-verifier', 'not-a-valid-sealed-value');
|
|
415
553
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
554
|
+
request.nextUrl.searchParams.set('state', 'not-a-valid-sealed-value');
|
|
416
555
|
|
|
417
556
|
const handler = handleAuth();
|
|
418
|
-
await handler(request);
|
|
557
|
+
const response = await handler(request);
|
|
419
558
|
|
|
420
|
-
expect(
|
|
421
|
-
|
|
422
|
-
code: 'test-code',
|
|
423
|
-
codeVerifier: undefined,
|
|
424
|
-
}),
|
|
425
|
-
);
|
|
559
|
+
expect(response.status).toBe(500);
|
|
560
|
+
expect(workos.userManagement.authenticateWithCode).not.toHaveBeenCalled();
|
|
426
561
|
});
|
|
427
562
|
|
|
428
563
|
it('should delete PKCE cookie after successful authentication', async () => {
|
|
429
564
|
vi.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
|
|
430
565
|
|
|
431
|
-
const
|
|
432
|
-
{ codeVerifier: 'test-verifier-123' },
|
|
433
|
-
{ password: process.env.WORKOS_COOKIE_PASSWORD! },
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
request.cookies.set('wos-pkce-verifier', sealedVerifier);
|
|
566
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier-123' });
|
|
437
567
|
request.nextUrl.searchParams.set('code', 'test-code');
|
|
568
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
569
|
+
|
|
570
|
+
const handler = handleAuth();
|
|
571
|
+
const response = await handler(request);
|
|
572
|
+
|
|
573
|
+
// The response should be a redirect (success) and have a Set-Cookie header to delete the PKCE cookie
|
|
574
|
+
expect(response.status).toBe(307);
|
|
575
|
+
|
|
576
|
+
const setCookieHeaders = response.headers.getSetCookie();
|
|
577
|
+
const pkceDeletionCookie = setCookieHeaders.find((c: string) => c.startsWith('wos-auth-verifier='));
|
|
578
|
+
expect(pkceDeletionCookie).toBeDefined();
|
|
579
|
+
expect(pkceDeletionCookie).toContain('Max-Age=0');
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('should delete PKCE cookie after failed authentication', async () => {
|
|
583
|
+
vi.mocked(workos.userManagement.authenticateWithCode).mockRejectedValue(new Error('Auth failed'));
|
|
584
|
+
|
|
585
|
+
const sealedState = await setAuthCookie(request, { nonce: 'foo', codeVerifier: 'test-verifier-123' });
|
|
586
|
+
request.nextUrl.searchParams.set('code', 'bad-code');
|
|
587
|
+
request.nextUrl.searchParams.set('state', sealedState);
|
|
438
588
|
|
|
439
589
|
const handler = handleAuth();
|
|
440
590
|
const response = await handler(request);
|
|
441
591
|
|
|
442
|
-
|
|
592
|
+
expect(response.status).toBe(500);
|
|
443
593
|
const setCookieHeaders = response.headers.getSetCookie();
|
|
444
|
-
const pkceDeletionCookie = setCookieHeaders.find((c: string) => c.startsWith('wos-
|
|
594
|
+
const pkceDeletionCookie = setCookieHeaders.find((c: string) => c.startsWith('wos-auth-verifier='));
|
|
445
595
|
expect(pkceDeletionCookie).toBeDefined();
|
|
446
596
|
expect(pkceDeletionCookie).toContain('Max-Age=0');
|
|
447
597
|
});
|