@workos-inc/authkit-nextjs 2.12.2 → 2.14.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 +138 -73
- package/dist/esm/errors.js +33 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +7 -2
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middleware-helpers.js +99 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/session.js +11 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/index.d.ts +3 -1
- package/dist/esm/types/middleware-helpers.d.ts +25 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/validate-api-key.d.ts +1 -1
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +0 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/workos.js +1 -1
- package/package.json +20 -21
- package/src/actions.spec.ts +14 -12
- package/src/auth.spec.ts +27 -29
- package/src/authkit-callback-route.spec.ts +31 -29
- package/src/components/authkit-provider.spec.tsx +67 -71
- package/src/components/button.spec.tsx +4 -6
- package/src/components/impersonation.spec.tsx +25 -25
- package/src/components/min-max-button.spec.tsx +2 -1
- package/src/components/tokenStore.spec.ts +21 -21
- package/src/components/useAccessToken.spec.tsx +73 -77
- package/src/components/useTokenClaims.spec.tsx +22 -22
- package/src/cookie.spec.ts +14 -9
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.spec.ts +12 -13
- package/src/get-authorization-url.ts +6 -10
- package/src/index.ts +16 -2
- package/src/middleware-helpers.spec.ts +231 -0
- package/src/middleware-helpers.ts +130 -0
- package/src/session.spec.ts +81 -73
- package/src/session.ts +16 -38
- package/src/utils.spec.ts +14 -31
- package/src/utils.ts +0 -2
- package/src/validate-api-key.spec.ts +4 -6
- package/src/workos.spec.ts +2 -2
- package/src/workos.ts +1 -1
package/src/session.spec.ts
CHANGED
|
@@ -10,12 +10,15 @@ import { jwtVerify } from 'jose';
|
|
|
10
10
|
import { sealData } from 'iron-session';
|
|
11
11
|
import { User } from '@workos-inc/node';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
vi.mock('jose', async () => {
|
|
14
|
+
const actual = await vi.importActual<typeof import('jose')>('jose');
|
|
15
|
+
return {
|
|
16
|
+
jwtVerify: vi.fn(),
|
|
17
|
+
createRemoteJWKSet: vi.fn(),
|
|
18
|
+
SignJWT: actual.SignJWT,
|
|
19
|
+
decodeJwt: actual.decodeJwt,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
19
22
|
|
|
20
23
|
// logging is disabled by default, flip this to true to still have logs in the console
|
|
21
24
|
const DEBUG = false;
|
|
@@ -47,11 +50,11 @@ describe('session.ts', () => {
|
|
|
47
50
|
} as User,
|
|
48
51
|
};
|
|
49
52
|
|
|
50
|
-
let consoleLogSpy:
|
|
53
|
+
let consoleLogSpy: MockInstance;
|
|
51
54
|
|
|
52
55
|
beforeEach(async () => {
|
|
53
56
|
// Clear all mocks between tests
|
|
54
|
-
|
|
57
|
+
vi.clearAllMocks();
|
|
55
58
|
|
|
56
59
|
// Reset the cookie store
|
|
57
60
|
const nextCookies = await cookies();
|
|
@@ -63,9 +66,9 @@ describe('session.ts', () => {
|
|
|
63
66
|
nextHeaders._reset();
|
|
64
67
|
nextHeaders.set('x-workos-middleware', 'true');
|
|
65
68
|
|
|
66
|
-
(jwtVerify as
|
|
69
|
+
(jwtVerify as Mock).mockReset();
|
|
67
70
|
|
|
68
|
-
consoleLogSpy =
|
|
71
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
69
72
|
if (DEBUG) {
|
|
70
73
|
console.info(...args);
|
|
71
74
|
}
|
|
@@ -74,7 +77,7 @@ describe('session.ts', () => {
|
|
|
74
77
|
|
|
75
78
|
afterEach(() => {
|
|
76
79
|
consoleLogSpy.mockRestore();
|
|
77
|
-
|
|
80
|
+
vi.resetModules();
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
describe('withAuth', () => {
|
|
@@ -159,7 +162,7 @@ describe('session.ts', () => {
|
|
|
159
162
|
it('should throw an error if the redirect URI is not set', async () => {
|
|
160
163
|
const originalWorkosRedirectUri = envVariables.WORKOS_REDIRECT_URI;
|
|
161
164
|
|
|
162
|
-
|
|
165
|
+
Object.defineProperty(envVariables, 'WORKOS_REDIRECT_URI', { value: '', configurable: true });
|
|
163
166
|
|
|
164
167
|
await expect(async () => {
|
|
165
168
|
await updateSessionMiddleware(
|
|
@@ -174,13 +177,16 @@ describe('session.ts', () => {
|
|
|
174
177
|
);
|
|
175
178
|
}).rejects.toThrow('You must provide a redirect URI in the AuthKit middleware or in the environment variables.');
|
|
176
179
|
|
|
177
|
-
|
|
180
|
+
Object.defineProperty(envVariables, 'WORKOS_REDIRECT_URI', {
|
|
181
|
+
value: originalWorkosRedirectUri,
|
|
182
|
+
configurable: true,
|
|
183
|
+
});
|
|
178
184
|
});
|
|
179
185
|
|
|
180
186
|
it('should throw an error if the cookie password is not set', async () => {
|
|
181
187
|
const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;
|
|
182
188
|
|
|
183
|
-
|
|
189
|
+
Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', { value: '', configurable: true });
|
|
184
190
|
|
|
185
191
|
await expect(async () => {
|
|
186
192
|
await updateSessionMiddleware(
|
|
@@ -197,13 +203,16 @@ describe('session.ts', () => {
|
|
|
197
203
|
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
|
|
198
204
|
);
|
|
199
205
|
|
|
200
|
-
|
|
206
|
+
Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', {
|
|
207
|
+
value: originalWorkosCookiePassword,
|
|
208
|
+
configurable: true,
|
|
209
|
+
});
|
|
201
210
|
});
|
|
202
211
|
|
|
203
212
|
it('should throw an error if the cookie password is less than 32 characters', async () => {
|
|
204
213
|
const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;
|
|
205
214
|
|
|
206
|
-
|
|
215
|
+
Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', { value: 'short', configurable: true });
|
|
207
216
|
|
|
208
217
|
await expect(async () => {
|
|
209
218
|
await updateSessionMiddleware(
|
|
@@ -220,7 +229,10 @@ describe('session.ts', () => {
|
|
|
220
229
|
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
|
|
221
230
|
);
|
|
222
231
|
|
|
223
|
-
|
|
232
|
+
Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', {
|
|
233
|
+
value: originalWorkosCookiePassword,
|
|
234
|
+
configurable: true,
|
|
235
|
+
});
|
|
224
236
|
});
|
|
225
237
|
|
|
226
238
|
it('should return early if there is no session', async () => {
|
|
@@ -241,7 +253,7 @@ describe('session.ts', () => {
|
|
|
241
253
|
});
|
|
242
254
|
|
|
243
255
|
it('should return 200 if the session is valid', async () => {
|
|
244
|
-
|
|
256
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
245
257
|
|
|
246
258
|
const nextCookies = await cookies();
|
|
247
259
|
nextCookies.set(
|
|
@@ -249,7 +261,7 @@ describe('session.ts', () => {
|
|
|
249
261
|
await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
|
|
250
262
|
);
|
|
251
263
|
|
|
252
|
-
(jwtVerify as
|
|
264
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
253
265
|
return true;
|
|
254
266
|
});
|
|
255
267
|
|
|
@@ -272,11 +284,11 @@ describe('session.ts', () => {
|
|
|
272
284
|
it('should attempt to refresh the session when the access token is invalid', async () => {
|
|
273
285
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
274
286
|
|
|
275
|
-
(jwtVerify as
|
|
287
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
276
288
|
throw new Error('Invalid token');
|
|
277
289
|
});
|
|
278
290
|
|
|
279
|
-
|
|
291
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
|
|
280
292
|
accessToken: await generateTestToken(),
|
|
281
293
|
refreshToken: 'new-refresh-token',
|
|
282
294
|
user: mockSession.user,
|
|
@@ -308,17 +320,15 @@ describe('session.ts', () => {
|
|
|
308
320
|
});
|
|
309
321
|
|
|
310
322
|
it('should delete the cookie when refreshing fails', async () => {
|
|
311
|
-
|
|
323
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
312
324
|
|
|
313
325
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
314
326
|
|
|
315
|
-
(jwtVerify as
|
|
327
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
316
328
|
throw new Error('Invalid token');
|
|
317
329
|
});
|
|
318
330
|
|
|
319
|
-
|
|
320
|
-
.spyOn(workos.userManagement, 'authenticateWithRefreshToken')
|
|
321
|
-
.mockRejectedValue(new Error('Failed to refresh'));
|
|
331
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Failed to refresh'));
|
|
322
332
|
|
|
323
333
|
const request = new NextRequest(new URL('http://example.com'));
|
|
324
334
|
|
|
@@ -354,7 +364,7 @@ describe('session.ts', () => {
|
|
|
354
364
|
|
|
355
365
|
describe('middleware auth', () => {
|
|
356
366
|
it('should redirect unauthenticated users on protected routes', async () => {
|
|
357
|
-
|
|
367
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
358
368
|
|
|
359
369
|
const request = new NextRequest(new URL('http://example.com/protected'));
|
|
360
370
|
const result = await updateSessionMiddleware(
|
|
@@ -374,10 +384,7 @@ describe('session.ts', () => {
|
|
|
374
384
|
);
|
|
375
385
|
});
|
|
376
386
|
|
|
377
|
-
it('should
|
|
378
|
-
const originalRedirect = NextResponse.redirect;
|
|
379
|
-
(NextResponse as Partial<typeof NextResponse>).redirect = undefined;
|
|
380
|
-
|
|
387
|
+
it('should return a redirect response when middlewareAuth is enabled and user is not authenticated', async () => {
|
|
381
388
|
const request = new NextRequest(new URL('http://example.com/protected'));
|
|
382
389
|
const result = await updateSessionMiddleware(
|
|
383
390
|
request,
|
|
@@ -390,10 +397,9 @@ describe('session.ts', () => {
|
|
|
390
397
|
[],
|
|
391
398
|
);
|
|
392
399
|
|
|
393
|
-
expect(result).toBeInstanceOf(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
(NextResponse as Partial<typeof NextResponse>).redirect = originalRedirect;
|
|
400
|
+
expect(result).toBeInstanceOf(NextResponse);
|
|
401
|
+
expect(result.status).toBe(307);
|
|
402
|
+
expect(result.headers.get('Location')).toContain('workos.com');
|
|
397
403
|
});
|
|
398
404
|
|
|
399
405
|
it('should automatically add the redirect URI to unauthenticatedPaths when middleware is enabled', async () => {
|
|
@@ -429,7 +435,7 @@ describe('session.ts', () => {
|
|
|
429
435
|
expect(result.headers.get('Location')).toContain('screen_hint=sign-up');
|
|
430
436
|
});
|
|
431
437
|
|
|
432
|
-
it('should
|
|
438
|
+
it('should not leak sign-up paths header to the browser', async () => {
|
|
433
439
|
const request = new NextRequest(new URL('http://example.com/protected-signup'));
|
|
434
440
|
const result = await updateSessionMiddleware(
|
|
435
441
|
request,
|
|
@@ -442,7 +448,8 @@ describe('session.ts', () => {
|
|
|
442
448
|
['/protected-signup'],
|
|
443
449
|
);
|
|
444
450
|
|
|
445
|
-
|
|
451
|
+
// x-sign-up-paths is an internal header that should not leak to the browser
|
|
452
|
+
expect(result.headers.get('x-sign-up-paths')).toBeNull();
|
|
446
453
|
});
|
|
447
454
|
|
|
448
455
|
it('should allow logged out users on unauthenticated paths', async () => {
|
|
@@ -479,11 +486,11 @@ describe('session.ts', () => {
|
|
|
479
486
|
|
|
480
487
|
it('should throw an error if the provided regex is invalid and a non-Error object is thrown', async () => {
|
|
481
488
|
// Reset modules to ensure clean import state
|
|
482
|
-
|
|
489
|
+
vi.resetModules();
|
|
483
490
|
|
|
484
491
|
// Import first, then spy
|
|
485
492
|
const pathToRegexp = await import('path-to-regexp');
|
|
486
|
-
const parseSpy =
|
|
493
|
+
const parseSpy = vi.spyOn(pathToRegexp, 'parse').mockImplementation(() => {
|
|
487
494
|
throw 'invalid regex';
|
|
488
495
|
});
|
|
489
496
|
|
|
@@ -507,6 +514,9 @@ describe('session.ts', () => {
|
|
|
507
514
|
|
|
508
515
|
// Verify the mock was called
|
|
509
516
|
expect(parseSpy).toHaveBeenCalled();
|
|
517
|
+
|
|
518
|
+
// Restore the spy to prevent leaking to subsequent tests
|
|
519
|
+
parseSpy.mockRestore();
|
|
510
520
|
});
|
|
511
521
|
|
|
512
522
|
it('should default to the WORKOS_REDIRECT_URI environment variable if no redirect URI is provided', async () => {
|
|
@@ -526,17 +536,17 @@ describe('session.ts', () => {
|
|
|
526
536
|
});
|
|
527
537
|
|
|
528
538
|
it('should delete the cookie and redirect when refreshing fails', async () => {
|
|
529
|
-
|
|
539
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
530
540
|
|
|
531
541
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
532
542
|
|
|
533
|
-
(jwtVerify as
|
|
543
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
534
544
|
throw new Error('Invalid token');
|
|
535
545
|
});
|
|
536
546
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
547
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(
|
|
548
|
+
new Error('Failed to refresh'),
|
|
549
|
+
);
|
|
540
550
|
|
|
541
551
|
const request = new NextRequest(new URL('http://example.com'));
|
|
542
552
|
|
|
@@ -637,12 +647,12 @@ describe('session.ts', () => {
|
|
|
637
647
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
638
648
|
|
|
639
649
|
// Mock token verification to fail
|
|
640
|
-
(jwtVerify as
|
|
650
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
641
651
|
throw new Error('Invalid token');
|
|
642
652
|
});
|
|
643
653
|
|
|
644
654
|
// Mock successful refresh
|
|
645
|
-
|
|
655
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
|
|
646
656
|
accessToken: await generateTestToken(),
|
|
647
657
|
refreshToken: 'new-refresh-token',
|
|
648
658
|
user: mockSession.user,
|
|
@@ -670,12 +680,12 @@ describe('session.ts', () => {
|
|
|
670
680
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
671
681
|
|
|
672
682
|
// Mock token verification to fail
|
|
673
|
-
(jwtVerify as
|
|
683
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
674
684
|
throw new Error('Invalid token');
|
|
675
685
|
});
|
|
676
686
|
|
|
677
687
|
// Mock refresh failure
|
|
678
|
-
|
|
688
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Refresh failed'));
|
|
679
689
|
|
|
680
690
|
const request = new NextRequest(new URL('http://example.com/protected'));
|
|
681
691
|
request.cookies.set(
|
|
@@ -697,15 +707,15 @@ describe('session.ts', () => {
|
|
|
697
707
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
698
708
|
|
|
699
709
|
// Mock token verification to fail
|
|
700
|
-
(jwtVerify as
|
|
710
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
701
711
|
throw new Error('Invalid token');
|
|
702
712
|
});
|
|
703
713
|
|
|
704
714
|
const newAccessToken = await generateTestToken();
|
|
705
|
-
const mockSuccessCallback =
|
|
715
|
+
const mockSuccessCallback = vi.fn();
|
|
706
716
|
|
|
707
717
|
// Mock successful refresh
|
|
708
|
-
|
|
718
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
|
|
709
719
|
accessToken: newAccessToken,
|
|
710
720
|
refreshToken: 'new-refresh-token',
|
|
711
721
|
user: mockSession.user,
|
|
@@ -736,15 +746,15 @@ describe('session.ts', () => {
|
|
|
736
746
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
737
747
|
|
|
738
748
|
// Mock token verification to fail
|
|
739
|
-
(jwtVerify as
|
|
749
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
740
750
|
throw new Error('Invalid token');
|
|
741
751
|
});
|
|
742
752
|
|
|
743
753
|
const mockError = new Error('Refresh failed');
|
|
744
|
-
const mockErrorCallback =
|
|
754
|
+
const mockErrorCallback = vi.fn();
|
|
745
755
|
|
|
746
756
|
// Mock refresh failure
|
|
747
|
-
|
|
757
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(mockError);
|
|
748
758
|
|
|
749
759
|
const request = new NextRequest(new URL('http://example.com/protected'));
|
|
750
760
|
request.cookies.set(
|
|
@@ -767,15 +777,15 @@ describe('session.ts', () => {
|
|
|
767
777
|
|
|
768
778
|
describe('refreshSession', () => {
|
|
769
779
|
it('should refresh session successfully', async () => {
|
|
770
|
-
|
|
780
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
|
|
771
781
|
accessToken: await generateTestToken(),
|
|
772
782
|
refreshToken: 'new-refresh-token',
|
|
773
783
|
user: mockSession.user,
|
|
774
784
|
});
|
|
775
785
|
|
|
776
|
-
|
|
777
|
-
.
|
|
778
|
-
|
|
786
|
+
vi.spyOn(workos.userManagement, 'getJwksUrl').mockReturnValue(
|
|
787
|
+
'https://api.workos.com/sso/jwks/client_1234567890',
|
|
788
|
+
);
|
|
779
789
|
|
|
780
790
|
const nextCookies = await cookies();
|
|
781
791
|
nextCookies.set(
|
|
@@ -811,15 +821,15 @@ describe('session.ts', () => {
|
|
|
811
821
|
await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
|
|
812
822
|
);
|
|
813
823
|
|
|
814
|
-
|
|
824
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
|
|
815
825
|
accessToken: await generateTestToken({ org_id: 'org_456' }),
|
|
816
826
|
refreshToken: 'new-refresh-token',
|
|
817
827
|
user: mockSession.user,
|
|
818
828
|
});
|
|
819
829
|
|
|
820
|
-
|
|
821
|
-
.
|
|
822
|
-
|
|
830
|
+
vi.spyOn(workos.userManagement, 'getJwksUrl').mockReturnValue(
|
|
831
|
+
'https://api.workos.com/sso/jwks/client_1234567890',
|
|
832
|
+
);
|
|
823
833
|
|
|
824
834
|
const result = await refreshSession({ organizationId: 'org_456' });
|
|
825
835
|
|
|
@@ -838,8 +848,8 @@ describe('session.ts', () => {
|
|
|
838
848
|
'wos-session',
|
|
839
849
|
await sealData(mockSessionWithValidJWT, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
|
|
840
850
|
);
|
|
841
|
-
|
|
842
|
-
expect(refreshSession({ ensureSignedIn: false })).rejects.toThrow('Failed to refresh session: fail');
|
|
851
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue('fail');
|
|
852
|
+
await expect(refreshSession({ ensureSignedIn: false })).rejects.toThrow('Failed to refresh session: fail');
|
|
843
853
|
});
|
|
844
854
|
|
|
845
855
|
it('throws if authenticateWithRefreshToken fails with error', async () => {
|
|
@@ -853,7 +863,7 @@ describe('session.ts', () => {
|
|
|
853
863
|
'wos-session',
|
|
854
864
|
await sealData(mockSessionWithValidJWT, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
|
|
855
865
|
);
|
|
856
|
-
|
|
866
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('error'));
|
|
857
867
|
await expect(refreshSession()).rejects.toThrow('Failed to refresh session: error');
|
|
858
868
|
});
|
|
859
869
|
});
|
|
@@ -863,7 +873,7 @@ describe('session.ts', () => {
|
|
|
863
873
|
const nextCookies = await cookies();
|
|
864
874
|
// @ts-expect-error - _reset is part of the mock
|
|
865
875
|
nextCookies._reset();
|
|
866
|
-
|
|
876
|
+
vi.clearAllMocks();
|
|
867
877
|
});
|
|
868
878
|
|
|
869
879
|
it('should return all token claims when accessToken is provided', async () => {
|
|
@@ -957,7 +967,7 @@ describe('session.ts', () => {
|
|
|
957
967
|
|
|
958
968
|
describe('eager auth functionality', () => {
|
|
959
969
|
beforeEach(() => {
|
|
960
|
-
|
|
970
|
+
vi.clearAllMocks();
|
|
961
971
|
});
|
|
962
972
|
|
|
963
973
|
describe('isInitialDocumentRequest', () => {
|
|
@@ -1051,12 +1061,12 @@ describe('session.ts', () => {
|
|
|
1051
1061
|
// Setup invalid session that needs refresh
|
|
1052
1062
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
1053
1063
|
|
|
1054
|
-
(jwtVerify as
|
|
1064
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
1055
1065
|
throw new Error('Invalid token');
|
|
1056
1066
|
});
|
|
1057
1067
|
|
|
1058
1068
|
const newAccessToken = await generateTestToken();
|
|
1059
|
-
|
|
1069
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockResolvedValue({
|
|
1060
1070
|
accessToken: newAccessToken,
|
|
1061
1071
|
refreshToken: 'new-refresh-token',
|
|
1062
1072
|
user: mockSession.user,
|
|
@@ -1082,13 +1092,11 @@ describe('session.ts', () => {
|
|
|
1082
1092
|
// Setup invalid session
|
|
1083
1093
|
mockSession.accessToken = await generateTestToken({}, true);
|
|
1084
1094
|
|
|
1085
|
-
(jwtVerify as
|
|
1095
|
+
(jwtVerify as Mock).mockImplementation(() => {
|
|
1086
1096
|
throw new Error('Invalid token');
|
|
1087
1097
|
});
|
|
1088
1098
|
|
|
1089
|
-
|
|
1090
|
-
.spyOn(workos.userManagement, 'authenticateWithRefreshToken')
|
|
1091
|
-
.mockRejectedValue(new Error('Refresh failed'));
|
|
1099
|
+
vi.spyOn(workos.userManagement, 'authenticateWithRefreshToken').mockRejectedValue(new Error('Refresh failed'));
|
|
1092
1100
|
|
|
1093
1101
|
const request = new NextRequest(new URL('http://example.com/page'));
|
|
1094
1102
|
request.headers.set('accept', 'text/html');
|
package/src/session.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { sealData, unsealData } from 'iron-session';
|
|
|
4
4
|
import { JWTPayload, createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
|
|
5
5
|
import { cookies, headers } from 'next/headers';
|
|
6
6
|
import { redirect } from 'next/navigation';
|
|
7
|
-
import { NextRequest
|
|
7
|
+
import { NextRequest } from 'next/server';
|
|
8
8
|
import { getCookieOptions, getJwtCookie } from './cookie.js';
|
|
9
9
|
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_NAME, WORKOS_COOKIE_PASSWORD, WORKOS_REDIRECT_URI } from './env-variables.js';
|
|
10
|
+
import { TokenRefreshError, getSessionErrorContext } from './errors.js';
|
|
10
11
|
import { getAuthorizationUrl } from './get-authorization-url.js';
|
|
11
12
|
import {
|
|
12
13
|
AccessToken,
|
|
@@ -21,7 +22,8 @@ import { getWorkOS } from './workos.js';
|
|
|
21
22
|
|
|
22
23
|
import type { AuthenticationResponse } from '@workos-inc/node';
|
|
23
24
|
import { parse, tokensToRegexp } from 'path-to-regexp';
|
|
24
|
-
import {
|
|
25
|
+
import { handleAuthkitHeaders } from './middleware-helpers.js';
|
|
26
|
+
import { lazy, setCachePreventionHeaders } from './utils.js';
|
|
25
27
|
|
|
26
28
|
const sessionHeaderName = 'x-workos-session';
|
|
27
29
|
const middlewareHeaderName = 'x-workos-middleware';
|
|
@@ -149,15 +151,6 @@ async function updateSessionMiddleware(
|
|
|
149
151
|
eagerAuth,
|
|
150
152
|
});
|
|
151
153
|
|
|
152
|
-
// If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
|
|
153
|
-
if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
|
|
154
|
-
if (debug) {
|
|
155
|
-
console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return redirectWithFallback(authorizationUrl as string, headers);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
154
|
// Record the sign up paths so we can use them later
|
|
162
155
|
if (signUpPaths.length > 0) {
|
|
163
156
|
headers.set(signUpPathsHeaderName, signUpPaths.join(','));
|
|
@@ -165,33 +158,16 @@ async function updateSessionMiddleware(
|
|
|
165
158
|
|
|
166
159
|
applyCacheSecurityHeaders(headers, request, session);
|
|
167
160
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
requestHeaders.set('x-redirect-uri', headers.get('x-redirect-uri')!);
|
|
174
|
-
}
|
|
175
|
-
if (headers.has(signUpPathsHeaderName)) {
|
|
176
|
-
requestHeaders.set(signUpPathsHeaderName, headers.get(signUpPathsHeaderName)!);
|
|
177
|
-
}
|
|
161
|
+
// If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
|
|
162
|
+
if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
|
|
163
|
+
if (debug) {
|
|
164
|
+
console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
|
|
165
|
+
}
|
|
178
166
|
|
|
179
|
-
|
|
180
|
-
// This ensures handlers see refreshed sessions immediately (before Set-Cookie reaches browser)
|
|
181
|
-
const sessionHeader = headers.get(sessionHeaderName);
|
|
182
|
-
if (sessionHeader) {
|
|
183
|
-
requestHeaders.set(sessionHeaderName, sessionHeader);
|
|
167
|
+
return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl as string });
|
|
184
168
|
}
|
|
185
169
|
|
|
186
|
-
|
|
187
|
-
headers.delete(sessionHeaderName);
|
|
188
|
-
|
|
189
|
-
return NextResponse.next({
|
|
190
|
-
request: {
|
|
191
|
-
headers: requestHeaders,
|
|
192
|
-
},
|
|
193
|
-
headers,
|
|
194
|
-
});
|
|
170
|
+
return handleAuthkitHeaders(request, headers);
|
|
195
171
|
}
|
|
196
172
|
|
|
197
173
|
async function updateSession(
|
|
@@ -406,9 +382,11 @@ async function refreshSession({
|
|
|
406
382
|
organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
|
|
407
383
|
});
|
|
408
384
|
} catch (error) {
|
|
409
|
-
throw new
|
|
410
|
-
|
|
411
|
-
|
|
385
|
+
throw new TokenRefreshError(
|
|
386
|
+
`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
|
|
387
|
+
error,
|
|
388
|
+
getSessionErrorContext(session),
|
|
389
|
+
);
|
|
412
390
|
}
|
|
413
391
|
|
|
414
392
|
const headersList = await headers();
|
package/src/utils.spec.ts
CHANGED
|
@@ -3,23 +3,21 @@ import { redirectWithFallback, errorResponseWithFallback } from './utils.js';
|
|
|
3
3
|
|
|
4
4
|
describe('utils', () => {
|
|
5
5
|
afterEach(() => {
|
|
6
|
-
|
|
6
|
+
vi.resetModules();
|
|
7
|
+
vi.restoreAllMocks();
|
|
7
8
|
});
|
|
8
9
|
|
|
9
10
|
describe('redirectWithFallback', () => {
|
|
10
11
|
it('uses NextResponse.redirect when available', () => {
|
|
11
12
|
const redirectUrl = 'https://example.com';
|
|
12
|
-
const mockRedirect =
|
|
13
|
-
const originalRedirect = NextResponse.redirect;
|
|
13
|
+
const mockRedirect = vi.fn().mockReturnValue('redirected');
|
|
14
14
|
|
|
15
|
-
NextResponse.
|
|
15
|
+
vi.spyOn(NextResponse, 'redirect').mockImplementation(mockRedirect);
|
|
16
16
|
|
|
17
17
|
const result = redirectWithFallback(redirectUrl);
|
|
18
18
|
|
|
19
19
|
expect(mockRedirect).toHaveBeenCalledWith(redirectUrl, { headers: undefined });
|
|
20
20
|
expect(result).toBe('redirected');
|
|
21
|
-
|
|
22
|
-
NextResponse.redirect = originalRedirect;
|
|
23
21
|
});
|
|
24
22
|
|
|
25
23
|
it('uses headers when provided', () => {
|
|
@@ -35,9 +33,9 @@ describe('utils', () => {
|
|
|
35
33
|
it('falls back to standard Response when NextResponse exists but redirect is undefined', async () => {
|
|
36
34
|
const redirectUrl = 'https://example.com';
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
vi.resetModules();
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
vi.doMock('next/server', () => ({
|
|
41
39
|
NextResponse: {
|
|
42
40
|
// exists but has no redirect method
|
|
43
41
|
},
|
|
@@ -55,10 +53,10 @@ describe('utils', () => {
|
|
|
55
53
|
it('falls back to standard Response when NextResponse is undefined', async () => {
|
|
56
54
|
const redirectUrl = 'https://example.com';
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
vi.resetModules();
|
|
59
57
|
|
|
60
58
|
// Mock with undefined NextResponse
|
|
61
|
-
|
|
59
|
+
vi.doMock('next/server', () => ({
|
|
62
60
|
NextResponse: undefined,
|
|
63
61
|
}));
|
|
64
62
|
|
|
@@ -81,8 +79,8 @@ describe('utils', () => {
|
|
|
81
79
|
};
|
|
82
80
|
|
|
83
81
|
it('uses NextResponse.json when available', () => {
|
|
84
|
-
const mockJson =
|
|
85
|
-
NextResponse.
|
|
82
|
+
const mockJson = vi.fn().mockReturnValue('error json response');
|
|
83
|
+
vi.spyOn(NextResponse, 'json').mockImplementation(mockJson);
|
|
86
84
|
|
|
87
85
|
const result = errorResponseWithFallback(errorBody);
|
|
88
86
|
|
|
@@ -90,25 +88,10 @@ describe('utils', () => {
|
|
|
90
88
|
expect(result).toBe('error json response');
|
|
91
89
|
});
|
|
92
90
|
|
|
93
|
-
it('falls back to standard Response when NextResponse is not available', () => {
|
|
94
|
-
const originalJson = NextResponse.json;
|
|
95
|
-
|
|
96
|
-
// @ts-expect-error - This is to test the fallback
|
|
97
|
-
delete NextResponse.json;
|
|
98
|
-
|
|
99
|
-
const result = errorResponseWithFallback(errorBody);
|
|
100
|
-
|
|
101
|
-
expect(result).toBeInstanceOf(Response);
|
|
102
|
-
expect(result.status).toBe(500);
|
|
103
|
-
expect(result.headers.get('Content-Type')).toBe('application/json');
|
|
104
|
-
|
|
105
|
-
NextResponse.json = originalJson;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
91
|
it('falls back to standard Response when NextResponse exists but json is undefined', async () => {
|
|
109
|
-
|
|
92
|
+
vi.resetModules();
|
|
110
93
|
|
|
111
|
-
|
|
94
|
+
vi.doMock('next/server', () => ({
|
|
112
95
|
NextResponse: {
|
|
113
96
|
// exists but has no json method
|
|
114
97
|
},
|
|
@@ -124,9 +107,9 @@ describe('utils', () => {
|
|
|
124
107
|
});
|
|
125
108
|
|
|
126
109
|
it('falls back to standard Response when NextResponse is undefined', async () => {
|
|
127
|
-
|
|
110
|
+
vi.resetModules();
|
|
128
111
|
|
|
129
|
-
|
|
112
|
+
vi.doMock('next/server', () => ({
|
|
130
113
|
NextResponse: undefined,
|
|
131
114
|
}));
|
|
132
115
|
|
package/src/utils.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { NextResponse } from 'next/server';
|
|
|
6
6
|
*/
|
|
7
7
|
export function setCachePreventionHeaders(headers: Headers): void {
|
|
8
8
|
headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
|
|
9
|
-
headers.set('Pragma', 'no-cache');
|
|
10
|
-
headers.set('Expires', '0');
|
|
11
9
|
headers.set('x-middleware-cache', 'no-cache');
|
|
12
10
|
}
|
|
13
11
|
|