@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -102
- package/dist/esm/actions.js +35 -5
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +71 -21
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +90 -92
- 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 +20 -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 +52 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +82 -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 +9 -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 +17 -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 +33 -34
- package/src/actions.spec.ts +91 -18
- package/src/actions.ts +44 -6
- package/src/auth.spec.ts +79 -29
- package/src/auth.ts +74 -42
- package/src/authkit-callback-route.spec.ts +372 -58
- package/src/authkit-callback-route.ts +121 -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 +63 -9
- package/src/cookie.ts +35 -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 +146 -0
- package/src/pkce.ts +59 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +104 -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/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
|
|
8
|
-
import { getCookieOptions, getJwtCookie } from './cookie.js';
|
|
7
|
+
import { NextRequest } from 'next/server';
|
|
8
|
+
import { getCookieOptions, getJwtCookie, getPKCECookieOptions } 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,
|
|
@@ -17,11 +18,30 @@ import {
|
|
|
17
18
|
Session,
|
|
18
19
|
UserInfo,
|
|
19
20
|
} from './interfaces.js';
|
|
21
|
+
import { getPKCECookieNameForState, setPKCECookie } from './pkce.js';
|
|
20
22
|
import { getWorkOS } from './workos.js';
|
|
21
23
|
|
|
22
24
|
import type { AuthenticationResponse } from '@workos-inc/node';
|
|
23
25
|
import { parse, tokensToRegexp } from 'path-to-regexp';
|
|
24
|
-
import {
|
|
26
|
+
import { handleAuthkitHeaders } from './middleware-helpers.js';
|
|
27
|
+
import { lazy, setCachePreventionHeaders } from './utils.js';
|
|
28
|
+
|
|
29
|
+
// Only set the PKCE cookie for initial document navigations — fetch/XHR/RSC/prefetch
|
|
30
|
+
// requests never follow cross-origin redirects so they'll never complete the OAuth
|
|
31
|
+
// flow and therefore don't need the cookie set.
|
|
32
|
+
// This prevents cookie bloat (HTTP 431) when multiple requests fire concurrently
|
|
33
|
+
// now that we are generating unique cookie names per flow, they add up quickly if
|
|
34
|
+
// we don't limit to just the initial navigation request
|
|
35
|
+
function appendPKCESetCookieHeader(request: NextRequest, headers: Headers, sealedState: string): void {
|
|
36
|
+
if (!isInitialDocumentRequest(request)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
headers.append(
|
|
41
|
+
'Set-Cookie',
|
|
42
|
+
`${getPKCECookieNameForState(sealedState)}=${sealedState}; ${getPKCECookieOptions(request.url, true)}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
25
45
|
|
|
26
46
|
const sessionHeaderName = 'x-workos-session';
|
|
27
47
|
const middlewareHeaderName = 'x-workos-middleware';
|
|
@@ -30,6 +50,49 @@ const jwtCookieName = 'workos-access-token';
|
|
|
30
50
|
|
|
31
51
|
const JWKS = lazy(() => createRemoteJWKSet(new URL(getWorkOS().userManagement.getJwksUrl(WORKOS_CLIENT_ID))));
|
|
32
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Applies cache security headers with Vary header deduplication.
|
|
55
|
+
* Only applies headers if the request is authenticated (has session, cookie, or Authorization header).
|
|
56
|
+
* Used in middleware where existing Vary headers may already be present.
|
|
57
|
+
* @param headers - The Headers object to set the cache security headers on.
|
|
58
|
+
* @param request - The NextRequest object to check for authentication.
|
|
59
|
+
* @param sessionData - Optional session data to check for authentication.
|
|
60
|
+
*/
|
|
61
|
+
function applyCacheSecurityHeaders(
|
|
62
|
+
headers: Headers,
|
|
63
|
+
request: NextRequest,
|
|
64
|
+
sessionData?: { accessToken?: string } | Session,
|
|
65
|
+
): void {
|
|
66
|
+
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
67
|
+
|
|
68
|
+
// Only apply cache headers for authenticated requests
|
|
69
|
+
if (!sessionData?.accessToken && !request.cookies.has(cookieName) && !request.headers.has('authorization')) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const varyValues = new Set<string>(['cookie']);
|
|
74
|
+
if (request.headers.has('authorization')) {
|
|
75
|
+
varyValues.add('authorization');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const currentVary = headers.get('Vary');
|
|
79
|
+
if (currentVary) {
|
|
80
|
+
currentVary.split(',').forEach((v) => {
|
|
81
|
+
const trimmed = v.trim().toLowerCase();
|
|
82
|
+
if (trimmed) varyValues.add(trimmed);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
headers.set(
|
|
87
|
+
'Vary',
|
|
88
|
+
Array.from(varyValues)
|
|
89
|
+
.map((v) => v.charAt(0).toUpperCase() + v.slice(1))
|
|
90
|
+
.join(', '),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
setCachePreventionHeaders(headers);
|
|
94
|
+
}
|
|
95
|
+
|
|
33
96
|
/**
|
|
34
97
|
* Determines if a request is for an initial document load (not API/RSC/prefetch)
|
|
35
98
|
*/
|
|
@@ -106,23 +169,23 @@ async function updateSessionMiddleware(
|
|
|
106
169
|
eagerAuth,
|
|
107
170
|
});
|
|
108
171
|
|
|
172
|
+
// Record the sign up paths so we can use them later
|
|
173
|
+
if (signUpPaths.length > 0) {
|
|
174
|
+
headers.set(signUpPathsHeaderName, signUpPaths.join(','));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
applyCacheSecurityHeaders(headers, request, session);
|
|
178
|
+
|
|
109
179
|
// If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
|
|
110
180
|
if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
|
|
111
181
|
if (debug) {
|
|
112
182
|
console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
|
|
113
183
|
}
|
|
114
184
|
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Record the sign up paths so we can use them later
|
|
119
|
-
if (signUpPaths.length > 0) {
|
|
120
|
-
headers.set(signUpPathsHeaderName, signUpPaths.join(','));
|
|
185
|
+
return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl as string });
|
|
121
186
|
}
|
|
122
187
|
|
|
123
|
-
return
|
|
124
|
-
headers,
|
|
125
|
-
});
|
|
188
|
+
return handleAuthkitHeaders(request, headers);
|
|
126
189
|
}
|
|
127
190
|
|
|
128
191
|
async function updateSession(
|
|
@@ -157,14 +220,18 @@ async function updateSession(
|
|
|
157
220
|
console.log('No session found from cookie');
|
|
158
221
|
}
|
|
159
222
|
|
|
223
|
+
const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
|
|
224
|
+
returnPathname: getReturnPathname(request.url),
|
|
225
|
+
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
226
|
+
screenHint: options.screenHint,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
appendPKCESetCookieHeader(request, newRequestHeaders, sealedState);
|
|
230
|
+
|
|
160
231
|
return {
|
|
161
232
|
session: { user: null },
|
|
162
233
|
headers: newRequestHeaders,
|
|
163
|
-
authorizationUrl
|
|
164
|
-
returnPathname: getReturnPathname(request.url),
|
|
165
|
-
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
166
|
-
screenHint: options.screenHint,
|
|
167
|
-
}),
|
|
234
|
+
authorizationUrl,
|
|
168
235
|
};
|
|
169
236
|
}
|
|
170
237
|
|
|
@@ -172,6 +239,8 @@ async function updateSession(
|
|
|
172
239
|
|
|
173
240
|
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
174
241
|
|
|
242
|
+
applyCacheSecurityHeaders(newRequestHeaders, request, session);
|
|
243
|
+
|
|
175
244
|
if (hasValidSession) {
|
|
176
245
|
newRequestHeaders.set(sessionHeaderName, request.cookies.get(cookieName)!.value);
|
|
177
246
|
|
|
@@ -293,13 +362,17 @@ async function updateSession(
|
|
|
293
362
|
|
|
294
363
|
options.onSessionRefreshError?.({ error: e, request });
|
|
295
364
|
|
|
365
|
+
const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
|
|
366
|
+
returnPathname: getReturnPathname(request.url),
|
|
367
|
+
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
appendPKCESetCookieHeader(request, newRequestHeaders, sealedState);
|
|
371
|
+
|
|
296
372
|
return {
|
|
297
373
|
session: { user: null },
|
|
298
374
|
headers: newRequestHeaders,
|
|
299
|
-
authorizationUrl
|
|
300
|
-
returnPathname: getReturnPathname(request.url),
|
|
301
|
-
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
302
|
-
}),
|
|
375
|
+
authorizationUrl,
|
|
303
376
|
};
|
|
304
377
|
}
|
|
305
378
|
}
|
|
@@ -335,9 +408,11 @@ async function refreshSession({
|
|
|
335
408
|
organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
|
|
336
409
|
});
|
|
337
410
|
} catch (error) {
|
|
338
|
-
throw new
|
|
339
|
-
|
|
340
|
-
|
|
411
|
+
throw new TokenRefreshError(
|
|
412
|
+
`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
|
|
413
|
+
error,
|
|
414
|
+
getSessionErrorContext(session),
|
|
415
|
+
);
|
|
341
416
|
}
|
|
342
417
|
|
|
343
418
|
const headersList = await headers();
|
|
@@ -404,7 +479,9 @@ async function redirectToSignIn() {
|
|
|
404
479
|
|
|
405
480
|
const returnPathname = getReturnPathname(url);
|
|
406
481
|
|
|
407
|
-
|
|
482
|
+
const { url: authkitUrl, sealedState } = await getAuthorizationUrl({ returnPathname, screenHint });
|
|
483
|
+
await setPKCECookie(sealedState);
|
|
484
|
+
redirect(authkitUrl);
|
|
408
485
|
}
|
|
409
486
|
|
|
410
487
|
export async function getTokenClaims<T = Record<string, unknown>>(
|
|
@@ -488,7 +565,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
|
|
|
488
565
|
if (!hasMiddleware) {
|
|
489
566
|
const url = headersList.get('x-url');
|
|
490
567
|
throw new Error(
|
|
491
|
-
`You are calling 'withAuth' on ${url ?? 'a route'} that isn
|
|
568
|
+
`You are calling 'withAuth' on ${url ?? 'a route'} that isn't covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`,
|
|
492
569
|
);
|
|
493
570
|
}
|
|
494
571
|
|
|
@@ -501,7 +578,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
|
|
|
501
578
|
function getReturnPathname(url: string): string {
|
|
502
579
|
const newUrl = new URL(url);
|
|
503
580
|
|
|
504
|
-
return `${newUrl.pathname}${newUrl.
|
|
581
|
+
return `${newUrl.pathname}${newUrl.search}`;
|
|
505
582
|
}
|
|
506
583
|
|
|
507
584
|
function getScreenHint(signUpPaths: string[] | undefined, pathname: string) {
|
package/src/test-helpers.ts
CHANGED
|
@@ -43,9 +43,9 @@ export async function generateSession(overrides: Partial<User> = {}) {
|
|
|
43
43
|
createdAt: '2024-01-01T00:00:00Z',
|
|
44
44
|
updatedAt: '2024-01-01T00:00:00Z',
|
|
45
45
|
lastSignInAt: '2024-01-01T00:00:00Z',
|
|
46
|
-
locale: null,
|
|
47
46
|
externalId: null,
|
|
48
47
|
metadata: {},
|
|
48
|
+
locale: null,
|
|
49
49
|
...overrides,
|
|
50
50
|
} satisfies User;
|
|
51
51
|
|
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
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Sets cache prevention headers to prevent CDN/proxy caching.
|
|
5
|
+
* @param headers - The Headers object to set the cache prevention headers on.
|
|
6
|
+
*/
|
|
7
|
+
export function setCachePreventionHeaders(headers: Headers): void {
|
|
8
|
+
headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0');
|
|
9
|
+
headers.set('x-middleware-cache', 'no-cache');
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
export function redirectWithFallback(redirectUri: string, headers?: Headers) {
|
|
4
13
|
const newHeaders = headers ? new Headers(headers) : new Headers();
|
|
5
14
|
newHeaders.set('Location', redirectUri);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { validateApiKey } from './validate-api-key.js';
|
|
2
|
+
import { getWorkOS } from './workos.js';
|
|
3
|
+
|
|
4
|
+
// These are mocked in vitest.setup.ts
|
|
5
|
+
import { headers } from 'next/headers';
|
|
6
|
+
|
|
7
|
+
const workos = getWorkOS();
|
|
8
|
+
|
|
9
|
+
describe('validate-api-key.ts', () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
// Clear all mocks between tests
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
|
|
14
|
+
const nextHeaders = await headers();
|
|
15
|
+
// @ts-expect-error - _reset is part of the mock
|
|
16
|
+
nextHeaders._reset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('validateApiKey', () => {
|
|
20
|
+
it('should return valid API key when Bearer token is present and valid', async () => {
|
|
21
|
+
const mockApiKeyResponse = {
|
|
22
|
+
apiKey: {
|
|
23
|
+
id: 'api_key_123',
|
|
24
|
+
object: 'api_key' as const,
|
|
25
|
+
name: 'Test API Key',
|
|
26
|
+
obfuscatedValue: 'sk_…7890',
|
|
27
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
28
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
29
|
+
lastUsedAt: '2024-01-01T00:00:00Z',
|
|
30
|
+
permissions: [],
|
|
31
|
+
owner: { type: 'organization' as const, id: 'org_123' },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
vi.spyOn(workos.apiKeys, 'validateApiKey').mockResolvedValue(mockApiKeyResponse);
|
|
36
|
+
|
|
37
|
+
const nextHeaders = await headers();
|
|
38
|
+
nextHeaders.set('authorization', 'Bearer sk_test_1234567890');
|
|
39
|
+
|
|
40
|
+
const result = await validateApiKey();
|
|
41
|
+
|
|
42
|
+
expect(workos.apiKeys.validateApiKey).toHaveBeenCalledWith({
|
|
43
|
+
value: 'sk_test_1234567890',
|
|
44
|
+
});
|
|
45
|
+
expect(result).toEqual(mockApiKeyResponse);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return { apiKey: null } when no authorization header is present', async () => {
|
|
49
|
+
// Don't set any authorization header
|
|
50
|
+
const result = await validateApiKey();
|
|
51
|
+
|
|
52
|
+
expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
|
|
53
|
+
expect(result).toEqual({ apiKey: null });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return { apiKey: null } when authorization header is empty', async () => {
|
|
57
|
+
const nextHeaders = await headers();
|
|
58
|
+
nextHeaders.set('authorization', '');
|
|
59
|
+
|
|
60
|
+
const result = await validateApiKey();
|
|
61
|
+
|
|
62
|
+
expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
|
|
63
|
+
expect(result).toEqual({ apiKey: null });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return { apiKey: null } when authorization header does not start with Bearer', async () => {
|
|
67
|
+
const nextHeaders = await headers();
|
|
68
|
+
nextHeaders.set('authorization', 'Basic dXNlcjpwYXNz');
|
|
69
|
+
|
|
70
|
+
const result = await validateApiKey();
|
|
71
|
+
|
|
72
|
+
expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
|
|
73
|
+
expect(result).toEqual({ apiKey: null });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return { apiKey: null } when Bearer token is missing', async () => {
|
|
77
|
+
const nextHeaders = await headers();
|
|
78
|
+
nextHeaders.set('authorization', 'Bearer');
|
|
79
|
+
|
|
80
|
+
const result = await validateApiKey();
|
|
81
|
+
|
|
82
|
+
expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
|
|
83
|
+
expect(result).toEqual({ apiKey: null });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return { apiKey: null } when Bearer token is only whitespace', async () => {
|
|
87
|
+
const nextHeaders = await headers();
|
|
88
|
+
nextHeaders.set('authorization', 'Bearer ');
|
|
89
|
+
|
|
90
|
+
const result = await validateApiKey();
|
|
91
|
+
|
|
92
|
+
expect(workos.apiKeys.validateApiKey).not.toHaveBeenCalled();
|
|
93
|
+
expect(result).toEqual({ apiKey: null });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return { apiKey: null } when WorkOS validation fails', async () => {
|
|
97
|
+
const mockResponse = { apiKey: null };
|
|
98
|
+
vi.spyOn(workos.apiKeys, 'validateApiKey').mockResolvedValue(mockResponse);
|
|
99
|
+
|
|
100
|
+
const nextHeaders = await headers();
|
|
101
|
+
nextHeaders.set('authorization', 'Bearer invalid_key');
|
|
102
|
+
|
|
103
|
+
const result = await validateApiKey();
|
|
104
|
+
|
|
105
|
+
expect(workos.apiKeys.validateApiKey).toHaveBeenCalledWith({
|
|
106
|
+
value: 'invalid_key',
|
|
107
|
+
});
|
|
108
|
+
expect(result).toEqual({ apiKey: null });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { getWorkOS } from './workos.js';
|
|
4
|
+
import { headers } from 'next/headers';
|
|
5
|
+
|
|
6
|
+
export async function validateApiKey() {
|
|
7
|
+
const headersList = await headers();
|
|
8
|
+
const authorizationHeader = headersList.get('authorization');
|
|
9
|
+
if (!authorizationHeader) {
|
|
10
|
+
return { apiKey: null };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const value = authorizationHeader.match(/Bearer\s+(.*)/i)?.[1];
|
|
14
|
+
if (!value) {
|
|
15
|
+
return { apiKey: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return getWorkOS().apiKeys.validateApiKey({ value });
|
|
19
|
+
}
|
package/src/workos.spec.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { getWorkOS, VERSION } from './workos.js';
|
|
|
4
4
|
describe('workos', () => {
|
|
5
5
|
const workos = getWorkOS();
|
|
6
6
|
beforeEach(() => {
|
|
7
|
-
|
|
7
|
+
vi.clearAllMocks();
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
it('initializes WorkOS with the correct configuration', () => {
|
|
@@ -35,7 +35,7 @@ describe('workos', () => {
|
|
|
35
35
|
const originalEnv = process.env;
|
|
36
36
|
|
|
37
37
|
beforeEach(() => {
|
|
38
|
-
|
|
38
|
+
vi.resetModules();
|
|
39
39
|
process.env = { ...originalEnv };
|
|
40
40
|
});
|
|
41
41
|
|
package/src/workos.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { WorkOS } from '@workos-inc/node';
|
|
|
2
2
|
import { WORKOS_API_HOSTNAME, WORKOS_API_KEY, WORKOS_API_HTTPS, WORKOS_API_PORT } from './env-variables.js';
|
|
3
3
|
import { lazy } from './utils.js';
|
|
4
4
|
|
|
5
|
-
export const VERSION = '2.
|
|
5
|
+
export const VERSION = '2.14.0';
|
|
6
6
|
|
|
7
7
|
const options = {
|
|
8
8
|
apiHostname: WORKOS_API_HOSTNAME,
|