@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/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,17 @@ import {
|
|
|
17
18
|
Session,
|
|
18
19
|
UserInfo,
|
|
19
20
|
} from './interfaces.js';
|
|
21
|
+
import { PKCE_COOKIE_NAME, 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
|
+
function appendPKCESetCookieHeader(headers: Headers, sealedState: string, requestUrl: string): void {
|
|
30
|
+
headers.append('Set-Cookie', `${PKCE_COOKIE_NAME}=${sealedState}; ${getPKCECookieOptions(requestUrl, true)}`);
|
|
31
|
+
}
|
|
25
32
|
|
|
26
33
|
const sessionHeaderName = 'x-workos-session';
|
|
27
34
|
const middlewareHeaderName = 'x-workos-middleware';
|
|
@@ -30,6 +37,49 @@ const jwtCookieName = 'workos-access-token';
|
|
|
30
37
|
|
|
31
38
|
const JWKS = lazy(() => createRemoteJWKSet(new URL(getWorkOS().userManagement.getJwksUrl(WORKOS_CLIENT_ID))));
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Applies cache security headers with Vary header deduplication.
|
|
42
|
+
* Only applies headers if the request is authenticated (has session, cookie, or Authorization header).
|
|
43
|
+
* Used in middleware where existing Vary headers may already be present.
|
|
44
|
+
* @param headers - The Headers object to set the cache security headers on.
|
|
45
|
+
* @param request - The NextRequest object to check for authentication.
|
|
46
|
+
* @param sessionData - Optional session data to check for authentication.
|
|
47
|
+
*/
|
|
48
|
+
function applyCacheSecurityHeaders(
|
|
49
|
+
headers: Headers,
|
|
50
|
+
request: NextRequest,
|
|
51
|
+
sessionData?: { accessToken?: string } | Session,
|
|
52
|
+
): void {
|
|
53
|
+
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
54
|
+
|
|
55
|
+
// Only apply cache headers for authenticated requests
|
|
56
|
+
if (!sessionData?.accessToken && !request.cookies.has(cookieName) && !request.headers.has('authorization')) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const varyValues = new Set<string>(['cookie']);
|
|
61
|
+
if (request.headers.has('authorization')) {
|
|
62
|
+
varyValues.add('authorization');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentVary = headers.get('Vary');
|
|
66
|
+
if (currentVary) {
|
|
67
|
+
currentVary.split(',').forEach((v) => {
|
|
68
|
+
const trimmed = v.trim().toLowerCase();
|
|
69
|
+
if (trimmed) varyValues.add(trimmed);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
headers.set(
|
|
74
|
+
'Vary',
|
|
75
|
+
Array.from(varyValues)
|
|
76
|
+
.map((v) => v.charAt(0).toUpperCase() + v.slice(1))
|
|
77
|
+
.join(', '),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
setCachePreventionHeaders(headers);
|
|
81
|
+
}
|
|
82
|
+
|
|
33
83
|
/**
|
|
34
84
|
* Determines if a request is for an initial document load (not API/RSC/prefetch)
|
|
35
85
|
*/
|
|
@@ -106,23 +156,23 @@ async function updateSessionMiddleware(
|
|
|
106
156
|
eagerAuth,
|
|
107
157
|
});
|
|
108
158
|
|
|
159
|
+
// Record the sign up paths so we can use them later
|
|
160
|
+
if (signUpPaths.length > 0) {
|
|
161
|
+
headers.set(signUpPathsHeaderName, signUpPaths.join(','));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
applyCacheSecurityHeaders(headers, request, session);
|
|
165
|
+
|
|
109
166
|
// If the user is logged out and this path isn't on the allowlist for logged out paths, redirect to AuthKit.
|
|
110
167
|
if (middlewareAuth.enabled && matchedPaths.length === 0 && !session.user) {
|
|
111
168
|
if (debug) {
|
|
112
169
|
console.log(`Unauthenticated user on protected route ${request.url}, redirecting to AuthKit`);
|
|
113
170
|
}
|
|
114
171
|
|
|
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(','));
|
|
172
|
+
return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl as string });
|
|
121
173
|
}
|
|
122
174
|
|
|
123
|
-
return
|
|
124
|
-
headers,
|
|
125
|
-
});
|
|
175
|
+
return handleAuthkitHeaders(request, headers);
|
|
126
176
|
}
|
|
127
177
|
|
|
128
178
|
async function updateSession(
|
|
@@ -157,14 +207,18 @@ async function updateSession(
|
|
|
157
207
|
console.log('No session found from cookie');
|
|
158
208
|
}
|
|
159
209
|
|
|
210
|
+
const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
|
|
211
|
+
returnPathname: getReturnPathname(request.url),
|
|
212
|
+
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
213
|
+
screenHint: options.screenHint,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
appendPKCESetCookieHeader(newRequestHeaders, sealedState, request.url);
|
|
217
|
+
|
|
160
218
|
return {
|
|
161
219
|
session: { user: null },
|
|
162
220
|
headers: newRequestHeaders,
|
|
163
|
-
authorizationUrl
|
|
164
|
-
returnPathname: getReturnPathname(request.url),
|
|
165
|
-
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
166
|
-
screenHint: options.screenHint,
|
|
167
|
-
}),
|
|
221
|
+
authorizationUrl,
|
|
168
222
|
};
|
|
169
223
|
}
|
|
170
224
|
|
|
@@ -172,6 +226,8 @@ async function updateSession(
|
|
|
172
226
|
|
|
173
227
|
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
|
|
174
228
|
|
|
229
|
+
applyCacheSecurityHeaders(newRequestHeaders, request, session);
|
|
230
|
+
|
|
175
231
|
if (hasValidSession) {
|
|
176
232
|
newRequestHeaders.set(sessionHeaderName, request.cookies.get(cookieName)!.value);
|
|
177
233
|
|
|
@@ -293,13 +349,17 @@ async function updateSession(
|
|
|
293
349
|
|
|
294
350
|
options.onSessionRefreshError?.({ error: e, request });
|
|
295
351
|
|
|
352
|
+
const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
|
|
353
|
+
returnPathname: getReturnPathname(request.url),
|
|
354
|
+
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
appendPKCESetCookieHeader(newRequestHeaders, sealedState, request.url);
|
|
358
|
+
|
|
296
359
|
return {
|
|
297
360
|
session: { user: null },
|
|
298
361
|
headers: newRequestHeaders,
|
|
299
|
-
authorizationUrl
|
|
300
|
-
returnPathname: getReturnPathname(request.url),
|
|
301
|
-
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
302
|
-
}),
|
|
362
|
+
authorizationUrl,
|
|
303
363
|
};
|
|
304
364
|
}
|
|
305
365
|
}
|
|
@@ -335,9 +395,11 @@ async function refreshSession({
|
|
|
335
395
|
organizationId: nextOrganizationId ?? organizationIdFromAccessToken,
|
|
336
396
|
});
|
|
337
397
|
} catch (error) {
|
|
338
|
-
throw new
|
|
339
|
-
|
|
340
|
-
|
|
398
|
+
throw new TokenRefreshError(
|
|
399
|
+
`Failed to refresh session: ${error instanceof Error ? error.message : String(error)}`,
|
|
400
|
+
error,
|
|
401
|
+
getSessionErrorContext(session),
|
|
402
|
+
);
|
|
341
403
|
}
|
|
342
404
|
|
|
343
405
|
const headersList = await headers();
|
|
@@ -404,7 +466,9 @@ async function redirectToSignIn() {
|
|
|
404
466
|
|
|
405
467
|
const returnPathname = getReturnPathname(url);
|
|
406
468
|
|
|
407
|
-
|
|
469
|
+
const { url: authkitUrl, sealedState } = await getAuthorizationUrl({ returnPathname, screenHint });
|
|
470
|
+
await setPKCECookie(sealedState);
|
|
471
|
+
redirect(authkitUrl);
|
|
408
472
|
}
|
|
409
473
|
|
|
410
474
|
export async function getTokenClaims<T = Record<string, unknown>>(
|
|
@@ -488,7 +552,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
|
|
|
488
552
|
if (!hasMiddleware) {
|
|
489
553
|
const url = headersList.get('x-url');
|
|
490
554
|
throw new Error(
|
|
491
|
-
`You are calling 'withAuth' on ${url ?? 'a route'} that isn
|
|
555
|
+
`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
556
|
);
|
|
493
557
|
}
|
|
494
558
|
|
|
@@ -501,7 +565,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
|
|
|
501
565
|
function getReturnPathname(url: string): string {
|
|
502
566
|
const newUrl = new URL(url);
|
|
503
567
|
|
|
504
|
-
return `${newUrl.pathname}${newUrl.
|
|
568
|
+
return `${newUrl.pathname}${newUrl.search}`;
|
|
505
569
|
}
|
|
506
570
|
|
|
507
571
|
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,
|