@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.
Files changed (84) hide show
  1. package/README.md +40 -11
  2. package/dist/esm/actions.js +35 -4
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +13 -22
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +71 -95
  7. package/dist/esm/authkit-callback-route.js.map +1 -1
  8. package/dist/esm/components/authkit-provider.js +31 -13
  9. package/dist/esm/components/authkit-provider.js.map +1 -1
  10. package/dist/esm/components/impersonation.js +9 -9
  11. package/dist/esm/components/impersonation.js.map +1 -1
  12. package/dist/esm/components/min-max-button.js +1 -1
  13. package/dist/esm/components/min-max-button.js.map +1 -1
  14. package/dist/esm/components/tokenStore.js +28 -19
  15. package/dist/esm/components/tokenStore.js.map +1 -1
  16. package/dist/esm/components/useAccessToken.js +1 -1
  17. package/dist/esm/components/useAccessToken.js.map +1 -1
  18. package/dist/esm/components/useTokenClaims.js +1 -1
  19. package/dist/esm/components/useTokenClaims.js.map +1 -1
  20. package/dist/esm/cookie.js +16 -5
  21. package/dist/esm/cookie.js.map +1 -1
  22. package/dist/esm/env-variables.js +5 -7
  23. package/dist/esm/env-variables.js.map +1 -1
  24. package/dist/esm/errors.js +7 -4
  25. package/dist/esm/errors.js.map +1 -1
  26. package/dist/esm/get-authorization-url.js +23 -27
  27. package/dist/esm/get-authorization-url.js.map +1 -1
  28. package/dist/esm/index.js +3 -3
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/interfaces.js +7 -1
  31. package/dist/esm/interfaces.js.map +1 -1
  32. package/dist/esm/middleware-helpers.js +8 -5
  33. package/dist/esm/middleware-helpers.js.map +1 -1
  34. package/dist/esm/middleware.js +3 -1
  35. package/dist/esm/middleware.js.map +1 -1
  36. package/dist/esm/pkce.js +17 -22
  37. package/dist/esm/pkce.js.map +1 -1
  38. package/dist/esm/session.js +19 -23
  39. package/dist/esm/session.js.map +1 -1
  40. package/dist/esm/types/actions.d.ts +34 -5
  41. package/dist/esm/types/auth.d.ts +6 -16
  42. package/dist/esm/types/cookie.d.ts +8 -0
  43. package/dist/esm/types/env-variables.d.ts +1 -2
  44. package/dist/esm/types/get-authorization-url.d.ts +1 -1
  45. package/dist/esm/types/index.d.ts +3 -3
  46. package/dist/esm/types/interfaces.d.ts +9 -1
  47. package/dist/esm/types/jwt.d.ts +9 -9
  48. package/dist/esm/types/middleware-helpers.d.ts +3 -1
  49. package/dist/esm/types/middleware.d.ts +3 -1
  50. package/dist/esm/types/pkce.d.ts +6 -5
  51. package/dist/esm/utils.js +2 -2
  52. package/dist/esm/utils.js.map +1 -1
  53. package/dist/esm/validate-api-key.js +1 -2
  54. package/dist/esm/validate-api-key.js.map +1 -1
  55. package/package.json +12 -13
  56. package/src/actions.spec.ts +81 -6
  57. package/src/actions.ts +44 -5
  58. package/src/auth.spec.ts +3 -2
  59. package/src/auth.ts +12 -43
  60. package/src/authkit-callback-route.spec.ts +210 -60
  61. package/src/authkit-callback-route.ts +94 -107
  62. package/src/components/authkit-provider.spec.tsx +89 -6
  63. package/src/components/authkit-provider.tsx +20 -1
  64. package/src/components/impersonation.spec.tsx +1 -0
  65. package/src/components/impersonation.tsx +29 -24
  66. package/src/components/tokenStore.spec.ts +35 -20
  67. package/src/components/tokenStore.ts +11 -3
  68. package/src/components/useAccessToken.spec.tsx +15 -12
  69. package/src/components/useTokenClaims.spec.tsx +1 -0
  70. package/src/cookie.ts +29 -0
  71. package/src/env-variables.ts +0 -2
  72. package/src/get-authorization-url.spec.ts +18 -40
  73. package/src/get-authorization-url.ts +34 -40
  74. package/src/index.ts +3 -1
  75. package/src/interfaces.ts +11 -1
  76. package/src/jwt.ts +9 -9
  77. package/src/middleware-helpers.spec.ts +7 -0
  78. package/src/middleware-helpers.ts +7 -3
  79. package/src/middleware.spec.ts +25 -0
  80. package/src/middleware.ts +4 -1
  81. package/src/pkce.spec.ts +125 -0
  82. package/src/pkce.ts +19 -19
  83. package/src/session.spec.ts +18 -22
  84. 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 { getCookieOptions } from './cookie.js';
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-pkce-verifier';
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(pkceCookieValue: string | undefined): Promise<void> {
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 } = getCookieOptions();
17
- nextCookies.set(PKCE_COOKIE_NAME, pkceCookieValue, {
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 from the cookie.
29
- * Returns undefined if the cookie is missing or corrupted.
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 getPKCECodeVerifier(cookieValue: string | undefined): Promise<string | undefined> {
32
- if (!cookieValue) return undefined;
33
- try {
34
- const unsealed = await unsealData<{ codeVerifier: string }>(cookieValue, {
35
- password: WORKOS_COOKIE_PASSWORD,
36
- });
37
- return unsealed.codeVerifier;
38
- } catch {
39
- // Cookie corrupted or expired — caller will proceed without PKCE
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
  }
@@ -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
- // URL-safe base64 encoding
151
- const pathname = encodeURIComponent(
152
- btoa(JSON.stringify({ returnPathname: '/protected?test=123' }))
153
- .replace(/\+/g, '-')
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(redirect).toHaveBeenCalledWith(expect.stringContaining(pathname));
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
- Object.defineProperty(envVariables, 'WORKOS_REDIRECT_URI', { value: '', configurable: true });
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
- Object.defineProperty(envVariables, 'WORKOS_REDIRECT_URI', {
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
- Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', { value: '', configurable: true });
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
- Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', {
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
- Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', { value: 'short', configurable: true });
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
- Object.defineProperty(envVariables, 'WORKOS_COOKIE_PASSWORD', {
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, pkceCookieValue: string | undefined, requestUrl: string): void {
30
- if (pkceCookieValue) {
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, pkceCookieValue } = await getAuthorizationUrl({
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, pkceCookieValue, request.url);
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, pkceCookieValue } = await getAuthorizationUrl({
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, pkceCookieValue, request.url);
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, pkceCookieValue } = await getAuthorizationUrl({ returnPathname, screenHint });
472
- await setPKCECookie(pkceCookieValue);
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.searchParams.size > 0 ? '?' + newUrl.searchParams.toString() : ''}`;
568
+ return `${newUrl.pathname}${newUrl.search}`;
571
569
  }
572
570
 
573
571
  function getScreenHint(signUpPaths: string[] | undefined, pathname: string) {