@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
package/src/pkce.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { unsealData } from 'iron-session';
|
|
2
2
|
import { cookies } from 'next/headers';
|
|
3
|
-
import
|
|
3
|
+
import * as v from 'valibot';
|
|
4
|
+
import { getPKCECookieOptions } from './cookie.js';
|
|
4
5
|
import { WORKOS_COOKIE_PASSWORD } from './env-variables.js';
|
|
6
|
+
import { State, StateSchema } from './interfaces.js';
|
|
5
7
|
|
|
6
|
-
export const PKCE_COOKIE_NAME = 'wos-
|
|
8
|
+
export const PKCE_COOKIE_NAME = 'wos-auth-verifier';
|
|
7
9
|
const PKCE_COOKIE_MAX_AGE = 600; // 10 minutes
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Set the PKCE verifier cookie in server action context.
|
|
11
13
|
* In middleware context, callers must set the cookie via Set-Cookie headers instead.
|
|
12
14
|
*/
|
|
13
|
-
export async function setPKCECookie(
|
|
14
|
-
if (!pkceCookieValue) return;
|
|
15
|
+
export async function setPKCECookie(sealedState: string): Promise<void> {
|
|
15
16
|
const nextCookies = await cookies();
|
|
16
|
-
const { domain, path, sameSite, secure } =
|
|
17
|
-
|
|
17
|
+
const { domain, path, sameSite, secure } = getPKCECookieOptions();
|
|
18
|
+
|
|
19
|
+
nextCookies.set(PKCE_COOKIE_NAME, sealedState, {
|
|
18
20
|
domain,
|
|
19
21
|
path,
|
|
20
22
|
sameSite,
|
|
@@ -25,18 +27,16 @@ export async function setPKCECookie(pkceCookieValue: string | undefined): Promis
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
|
-
* Read and unseal the PKCE code verifier
|
|
29
|
-
*
|
|
30
|
+
* Read and unseal the auth cookie containing PKCE code verifier and OAuth state.
|
|
31
|
+
* Throws if the cookie is not in the required state
|
|
30
32
|
*/
|
|
31
|
-
export async function
|
|
32
|
-
if
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
33
|
+
export async function getStateFromPKCECookieValue(cookieValue: string): Promise<State> {
|
|
34
|
+
// NOTE: TypeScript compiler won't flag if we Seal different data in than we Unseal
|
|
35
|
+
// Also, this function is not in a critically-high-performance path, so runtime validation
|
|
36
|
+
// is an acceptable tradeoff for increased security and type-safety
|
|
37
|
+
const unsealed = await unsealData(cookieValue, {
|
|
38
|
+
password: WORKOS_COOKIE_PASSWORD,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return v.parse(StateSchema, unsealed);
|
|
42
42
|
}
|
package/src/session.spec.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Mock, MockInstance } from 'vitest';
|
|
1
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
3
|
import { cookies, headers } from 'next/headers';
|
|
3
4
|
import { redirect } from 'next/navigation';
|
|
@@ -7,8 +8,14 @@ import { getWorkOS } from './workos.js';
|
|
|
7
8
|
import * as envVariables from './env-variables.js';
|
|
8
9
|
|
|
9
10
|
import { jwtVerify } from 'jose';
|
|
11
|
+
|
|
12
|
+
// Helper to override env variable exports without triggering no-import-assign on the import binding
|
|
13
|
+
function setEnvVar(mod: Record<string, unknown>, key: string, value: unknown) {
|
|
14
|
+
Object.defineProperty(mod, key, { value, configurable: true });
|
|
15
|
+
}
|
|
10
16
|
import { sealData } from 'iron-session';
|
|
11
17
|
import { User } from '@workos-inc/node';
|
|
18
|
+
import { getStateFromPKCECookieValue } from './pkce.js';
|
|
12
19
|
|
|
13
20
|
vi.mock('jose', async () => {
|
|
14
21
|
const actual = await vi.importActual<typeof import('jose')>('jose');
|
|
@@ -147,14 +154,12 @@ describe('session.ts', () => {
|
|
|
147
154
|
|
|
148
155
|
await withAuth({ ensureSignedIn: true });
|
|
149
156
|
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.replace(/\//g, '_'),
|
|
155
|
-
);
|
|
157
|
+
// The state is now sealed, se we need to unseal it
|
|
158
|
+
const redirectUrl = new URL((redirect as unknown as Mock).mock.calls[0][0]);
|
|
159
|
+
const sealedState = redirectUrl.searchParams.get('state')!;
|
|
160
|
+
const { returnPathname } = await getStateFromPKCECookieValue(sealedState);
|
|
156
161
|
|
|
157
|
-
expect(
|
|
162
|
+
expect(returnPathname).toBe('/protected?test=123');
|
|
158
163
|
});
|
|
159
164
|
});
|
|
160
165
|
|
|
@@ -162,7 +167,7 @@ describe('session.ts', () => {
|
|
|
162
167
|
it('should throw an error if the redirect URI is not set', async () => {
|
|
163
168
|
const originalWorkosRedirectUri = envVariables.WORKOS_REDIRECT_URI;
|
|
164
169
|
|
|
165
|
-
|
|
170
|
+
setEnvVar(envVariables, 'WORKOS_REDIRECT_URI', '');
|
|
166
171
|
|
|
167
172
|
await expect(async () => {
|
|
168
173
|
await updateSessionMiddleware(
|
|
@@ -177,16 +182,13 @@ describe('session.ts', () => {
|
|
|
177
182
|
);
|
|
178
183
|
}).rejects.toThrow('You must provide a redirect URI in the AuthKit middleware or in the environment variables.');
|
|
179
184
|
|
|
180
|
-
|
|
181
|
-
value: originalWorkosRedirectUri,
|
|
182
|
-
configurable: true,
|
|
183
|
-
});
|
|
185
|
+
setEnvVar(envVariables, 'WORKOS_REDIRECT_URI', originalWorkosRedirectUri);
|
|
184
186
|
});
|
|
185
187
|
|
|
186
188
|
it('should throw an error if the cookie password is not set', async () => {
|
|
187
189
|
const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
setEnvVar(envVariables, 'WORKOS_COOKIE_PASSWORD', '');
|
|
190
192
|
|
|
191
193
|
await expect(async () => {
|
|
192
194
|
await updateSessionMiddleware(
|
|
@@ -203,16 +205,13 @@ describe('session.ts', () => {
|
|
|
203
205
|
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
|
|
204
206
|
);
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
value: originalWorkosCookiePassword,
|
|
208
|
-
configurable: true,
|
|
209
|
-
});
|
|
208
|
+
setEnvVar(envVariables, 'WORKOS_COOKIE_PASSWORD', originalWorkosCookiePassword);
|
|
210
209
|
});
|
|
211
210
|
|
|
212
211
|
it('should throw an error if the cookie password is less than 32 characters', async () => {
|
|
213
212
|
const originalWorkosCookiePassword = envVariables.WORKOS_COOKIE_PASSWORD;
|
|
214
213
|
|
|
215
|
-
|
|
214
|
+
setEnvVar(envVariables, 'WORKOS_COOKIE_PASSWORD', 'short');
|
|
216
215
|
|
|
217
216
|
await expect(async () => {
|
|
218
217
|
await updateSessionMiddleware(
|
|
@@ -229,10 +228,7 @@ describe('session.ts', () => {
|
|
|
229
228
|
'You must provide a valid cookie password that is at least 32 characters in the environment variables.',
|
|
230
229
|
);
|
|
231
230
|
|
|
232
|
-
|
|
233
|
-
value: originalWorkosCookiePassword,
|
|
234
|
-
configurable: true,
|
|
235
|
-
});
|
|
231
|
+
setEnvVar(envVariables, 'WORKOS_COOKIE_PASSWORD', originalWorkosCookiePassword);
|
|
236
232
|
});
|
|
237
233
|
|
|
238
234
|
it('should return early if there is no session', async () => {
|
package/src/session.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { JWTPayload, createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose';
|
|
|
5
5
|
import { cookies, headers } from 'next/headers';
|
|
6
6
|
import { redirect } from 'next/navigation';
|
|
7
7
|
import { NextRequest } from 'next/server';
|
|
8
|
-
import { getCookieOptions, getJwtCookie } from './cookie.js';
|
|
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
10
|
import { TokenRefreshError, getSessionErrorContext } from './errors.js';
|
|
11
11
|
import { getAuthorizationUrl } from './get-authorization-url.js';
|
|
@@ -26,10 +26,8 @@ import { parse, tokensToRegexp } from 'path-to-regexp';
|
|
|
26
26
|
import { handleAuthkitHeaders } from './middleware-helpers.js';
|
|
27
27
|
import { lazy, setCachePreventionHeaders } from './utils.js';
|
|
28
28
|
|
|
29
|
-
function appendPKCESetCookieHeader(headers: Headers,
|
|
30
|
-
|
|
31
|
-
headers.append('Set-Cookie', `${PKCE_COOKIE_NAME}=${pkceCookieValue}; ${getCookieOptions(requestUrl, true)}`);
|
|
32
|
-
}
|
|
29
|
+
function appendPKCESetCookieHeader(headers: Headers, sealedState: string, requestUrl: string): void {
|
|
30
|
+
headers.append('Set-Cookie', `${PKCE_COOKIE_NAME}=${sealedState}; ${getPKCECookieOptions(requestUrl, true)}`);
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
const sessionHeaderName = 'x-workos-session';
|
|
@@ -209,13 +207,13 @@ async function updateSession(
|
|
|
209
207
|
console.log('No session found from cookie');
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
const { url: authorizationUrl,
|
|
210
|
+
const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
|
|
213
211
|
returnPathname: getReturnPathname(request.url),
|
|
214
212
|
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
215
213
|
screenHint: options.screenHint,
|
|
216
214
|
});
|
|
217
215
|
|
|
218
|
-
appendPKCESetCookieHeader(newRequestHeaders,
|
|
216
|
+
appendPKCESetCookieHeader(newRequestHeaders, sealedState, request.url);
|
|
219
217
|
|
|
220
218
|
return {
|
|
221
219
|
session: { user: null },
|
|
@@ -351,12 +349,12 @@ async function updateSession(
|
|
|
351
349
|
|
|
352
350
|
options.onSessionRefreshError?.({ error: e, request });
|
|
353
351
|
|
|
354
|
-
const { url: authorizationUrl,
|
|
352
|
+
const { url: authorizationUrl, sealedState } = await getAuthorizationUrl({
|
|
355
353
|
returnPathname: getReturnPathname(request.url),
|
|
356
354
|
redirectUri: options.redirectUri || WORKOS_REDIRECT_URI,
|
|
357
355
|
});
|
|
358
356
|
|
|
359
|
-
appendPKCESetCookieHeader(newRequestHeaders,
|
|
357
|
+
appendPKCESetCookieHeader(newRequestHeaders, sealedState, request.url);
|
|
360
358
|
|
|
361
359
|
return {
|
|
362
360
|
session: { user: null },
|
|
@@ -468,8 +466,8 @@ async function redirectToSignIn() {
|
|
|
468
466
|
|
|
469
467
|
const returnPathname = getReturnPathname(url);
|
|
470
468
|
|
|
471
|
-
const { url: authkitUrl,
|
|
472
|
-
await setPKCECookie(
|
|
469
|
+
const { url: authkitUrl, sealedState } = await getAuthorizationUrl({ returnPathname, screenHint });
|
|
470
|
+
await setPKCECookie(sealedState);
|
|
473
471
|
redirect(authkitUrl);
|
|
474
472
|
}
|
|
475
473
|
|
|
@@ -567,7 +565,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> {
|
|
|
567
565
|
function getReturnPathname(url: string): string {
|
|
568
566
|
const newUrl = new URL(url);
|
|
569
567
|
|
|
570
|
-
return `${newUrl.pathname}${newUrl.
|
|
568
|
+
return `${newUrl.pathname}${newUrl.search}`;
|
|
571
569
|
}
|
|
572
570
|
|
|
573
571
|
function getScreenHint(signUpPaths: string[] | undefined, pathname: string) {
|