@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.
Files changed (106) hide show
  1. package/README.md +305 -102
  2. package/dist/esm/actions.js +35 -5
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +71 -21
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +90 -92
  7. package/dist/esm/authkit-callback-route.js.map +1 -1
  8. package/dist/esm/components/authkit-provider.js +36 -15
  9. package/dist/esm/components/authkit-provider.js.map +1 -1
  10. package/dist/esm/components/impersonation.js +17 -15
  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 +20 -5
  21. package/dist/esm/cookie.js.map +1 -1
  22. package/dist/esm/env-variables.js +6 -6
  23. package/dist/esm/env-variables.js.map +1 -1
  24. package/dist/esm/errors.js +36 -0
  25. package/dist/esm/errors.js.map +1 -0
  26. package/dist/esm/get-authorization-url.js +51 -12
  27. package/dist/esm/get-authorization-url.js.map +1 -1
  28. package/dist/esm/index.js +5 -2
  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 +102 -0
  33. package/dist/esm/middleware-helpers.js.map +1 -0
  34. package/dist/esm/middleware.js +3 -1
  35. package/dist/esm/middleware.js.map +1 -1
  36. package/dist/esm/pkce.js +52 -0
  37. package/dist/esm/pkce.js.map +1 -0
  38. package/dist/esm/session.js +82 -35
  39. package/dist/esm/session.js.map +1 -1
  40. package/dist/esm/test-helpers.js +1 -1
  41. package/dist/esm/test-helpers.js.map +1 -1
  42. package/dist/esm/types/actions.d.ts +34 -5
  43. package/dist/esm/types/auth.d.ts +7 -15
  44. package/dist/esm/types/components/authkit-provider.d.ts +6 -2
  45. package/dist/esm/types/components/impersonation.d.ts +2 -1
  46. package/dist/esm/types/cookie.d.ts +9 -0
  47. package/dist/esm/types/env-variables.d.ts +2 -1
  48. package/dist/esm/types/errors.d.ts +15 -0
  49. package/dist/esm/types/get-authorization-url.d.ts +2 -2
  50. package/dist/esm/types/index.d.ts +5 -2
  51. package/dist/esm/types/interfaces.d.ts +12 -0
  52. package/dist/esm/types/jwt.d.ts +9 -9
  53. package/dist/esm/types/middleware-helpers.d.ts +27 -0
  54. package/dist/esm/types/middleware.d.ts +3 -1
  55. package/dist/esm/types/pkce.d.ts +17 -0
  56. package/dist/esm/types/session.d.ts +1 -1
  57. package/dist/esm/types/utils.d.ts +5 -0
  58. package/dist/esm/types/validate-api-key.d.ts +1 -0
  59. package/dist/esm/types/workos.d.ts +1 -1
  60. package/dist/esm/utils.js +10 -2
  61. package/dist/esm/utils.js.map +1 -1
  62. package/dist/esm/validate-api-key.js +16 -0
  63. package/dist/esm/validate-api-key.js.map +1 -0
  64. package/dist/esm/workos.js +1 -1
  65. package/package.json +33 -34
  66. package/src/actions.spec.ts +91 -18
  67. package/src/actions.ts +44 -6
  68. package/src/auth.spec.ts +79 -29
  69. package/src/auth.ts +74 -42
  70. package/src/authkit-callback-route.spec.ts +372 -58
  71. package/src/authkit-callback-route.ts +121 -103
  72. package/src/components/authkit-provider.spec.tsx +264 -70
  73. package/src/components/authkit-provider.tsx +40 -15
  74. package/src/components/button.spec.tsx +4 -6
  75. package/src/components/impersonation.spec.tsx +152 -35
  76. package/src/components/impersonation.tsx +37 -30
  77. package/src/components/min-max-button.spec.tsx +2 -1
  78. package/src/components/tokenStore.spec.ts +59 -44
  79. package/src/components/tokenStore.ts +11 -3
  80. package/src/components/useAccessToken.spec.tsx +82 -83
  81. package/src/components/useTokenClaims.spec.tsx +23 -22
  82. package/src/cookie.spec.ts +63 -9
  83. package/src/cookie.ts +35 -0
  84. package/src/env-variables.ts +2 -0
  85. package/src/errors.spec.ts +108 -0
  86. package/src/errors.ts +46 -0
  87. package/src/get-authorization-url.spec.ts +170 -15
  88. package/src/get-authorization-url.ts +69 -23
  89. package/src/index.ts +20 -2
  90. package/src/interfaces.ts +15 -0
  91. package/src/jwt.ts +9 -9
  92. package/src/middleware-helpers.spec.ts +238 -0
  93. package/src/middleware-helpers.ts +134 -0
  94. package/src/middleware.spec.ts +25 -0
  95. package/src/middleware.ts +4 -1
  96. package/src/pkce.spec.ts +146 -0
  97. package/src/pkce.ts +59 -0
  98. package/src/session.spec.ts +87 -89
  99. package/src/session.ts +104 -27
  100. package/src/test-helpers.ts +1 -1
  101. package/src/utils.spec.ts +14 -31
  102. package/src/utils.ts +9 -0
  103. package/src/validate-api-key.spec.ts +111 -0
  104. package/src/validate-api-key.ts +19 -0
  105. package/src/workos.spec.ts +2 -2
  106. package/src/workos.ts +1 -1
@@ -1,39 +1,15 @@
1
1
  import { NextRequest } from 'next/server';
2
+ import { getPKCECookieOptions } from './cookie.js';
2
3
  import { WORKOS_CLIENT_ID } from './env-variables.js';
3
4
  import { HandleAuthOptions } from './interfaces.js';
5
+ import { PKCE_COOKIE_NAME, getPKCECookieNameForState, getStateFromPKCECookieValue } from './pkce.js';
4
6
  import { saveSession } from './session.js';
5
- import { errorResponseWithFallback, redirectWithFallback } from './utils.js';
7
+ import { errorResponseWithFallback, redirectWithFallback, setCachePreventionHeaders } from './utils.js';
6
8
  import { getWorkOS } from './workos.js';
7
9
 
8
- function handleState(state: string | null) {
9
- let returnPathname: string | undefined = undefined;
10
- let userState: string | undefined;
11
- if (state?.includes('.')) {
12
- const [internal, ...rest] = state.split('.');
13
- userState = rest.join('.');
14
- try {
15
- // Reverse URL-safe base64 encoding
16
- const decoded = internal.replace(/-/g, '+').replace(/_/g, '/');
17
- returnPathname = JSON.parse(atob(decoded)).returnPathname;
18
- } catch {
19
- // Malformed internal part, ignore it
20
- }
21
- } else if (state) {
22
- try {
23
- const decoded = JSON.parse(atob(state));
24
- if (decoded.returnPathname) {
25
- returnPathname = decoded.returnPathname;
26
- } else {
27
- userState = state;
28
- }
29
- } catch {
30
- userState = state;
31
- }
32
- }
33
- return {
34
- returnPathname,
35
- state: userState,
36
- };
10
+ function preventCaching(headers: Headers): void {
11
+ headers.set('Vary', 'Cookie');
12
+ setCachePreventionHeaders(headers);
37
13
  }
38
14
 
39
15
  export function handleAuth(options: HandleAuthOptions = {}) {
@@ -49,90 +25,132 @@ export function handleAuth(options: HandleAuthOptions = {}) {
49
25
  }
50
26
 
51
27
  return async function GET(request: NextRequest) {
52
- const code = request.nextUrl.searchParams.get('code');
53
- const state = request.nextUrl.searchParams.get('state');
54
-
55
- const { state: customState, returnPathname: returnPathnameState } = handleState(state);
56
-
57
- if (code) {
58
- try {
59
- // Use the code returned to us by AuthKit and authenticate the user with WorkOS
60
- const { accessToken, refreshToken, user, impersonator, oauthTokens, authenticationMethod, organizationId } =
61
- await getWorkOS().userManagement.authenticateWithCode({
62
- clientId: WORKOS_CLIENT_ID,
63
- code,
64
- });
65
-
66
- // If baseURL is provided, use it instead of request.nextUrl
67
- // This is useful if the app is being run in a container like docker where
68
- // the hostname can be different from the one in the request
69
- const url = baseURL ? new URL(baseURL) : request.nextUrl.clone();
70
-
71
- // Cleanup params
72
- url.searchParams.delete('code');
73
- url.searchParams.delete('state');
74
-
75
- // Redirect to the requested path and store the session
76
- const returnPathname = returnPathnameState ?? returnPathnameOption;
77
-
78
- // Extract the search params if they are present
79
- if (returnPathname.includes('?')) {
80
- const newUrl = new URL(returnPathname, 'https://example.com');
81
- url.pathname = newUrl.pathname;
82
-
83
- for (const [key, value] of newUrl.searchParams) {
84
- url.searchParams.append(key, value);
85
- }
86
- } else {
87
- url.pathname = returnPathname;
88
- }
89
-
90
- // Fall back to standard Response if NextResponse is not available.
91
- // This is to support Next.js 13.
92
- const response = redirectWithFallback(url.toString());
93
-
94
- if (!accessToken || !refreshToken) throw new Error('response is missing tokens');
95
-
96
- await saveSession({ accessToken, refreshToken, user, impersonator }, request);
97
-
98
- if (onSuccess) {
99
- await onSuccess({
100
- accessToken,
101
- refreshToken,
102
- user,
103
- impersonator,
104
- oauthTokens,
105
- authenticationMethod,
106
- organizationId,
107
- state: customState,
108
- });
109
- }
110
-
111
- return response;
112
- } catch (error) {
113
- const errorRes = {
114
- error: error instanceof Error ? error.message : String(error),
115
- };
116
-
117
- console.error(errorRes);
118
-
119
- return errorResponse(request, error);
28
+ // Fall back to standard URL parsing when nextUrl is not available (e.g., vinext)
29
+ const requestUrl = request.nextUrl ?? new URL(request.url);
30
+
31
+ // Gather mandatory information
32
+ const code = requestUrl.searchParams.get('code');
33
+ const state = requestUrl.searchParams.get('state');
34
+
35
+ // We want to catch any & all errors and respond the same way, always
36
+ // destroying the 1-use PKCE cookie to prevent replay attacks or stale
37
+ // cookies affecting future auth attempts.
38
+ try {
39
+ if (!code || !state) {
40
+ throw new Error('Missing required auth parameter');
41
+ }
42
+
43
+ // Derive the flow-specific cookie name from the state param so each
44
+ // concurrent auth flow reads/deletes its own cookie, not a shared one.
45
+ // Fall back to the legacy shared cookie name so in-flight OAuth flows
46
+ // started on v3.0.x don't fail on the first callback after upgrade.
47
+ // Safe to remove once v3.0.x is unsupported.
48
+ const pkceCookieName = getPKCECookieNameForState(state);
49
+ const pkceCookie = request.cookies.get(pkceCookieName)?.value ?? request.cookies.get(PKCE_COOKIE_NAME)?.value;
50
+
51
+ // CSRF verification: both channels (cookie + URL state) must be present and match
52
+ if (!pkceCookie) {
53
+ throw new Error(
54
+ 'Auth cookie missing cannot verify OAuth state. Ensure Set-Cookie headers are propagated on redirects.',
55
+ );
56
+ }
57
+
58
+ if (state !== pkceCookie) {
59
+ throw new Error('OAuth state mismatch');
60
+ }
61
+
62
+ const {
63
+ codeVerifier,
64
+ customState,
65
+ returnPathname: returnPathnameState,
66
+ } = await getStateFromPKCECookieValue(pkceCookie);
67
+
68
+ // Use the code returned to us by AuthKit and authenticate the user with WorkOS
69
+ const { accessToken, refreshToken, user, impersonator, oauthTokens, authenticationMethod, organizationId } =
70
+ await getWorkOS().userManagement.authenticateWithCode({
71
+ clientId: WORKOS_CLIENT_ID,
72
+ code,
73
+ codeVerifier,
74
+ });
75
+
76
+ if (!accessToken || !refreshToken) {
77
+ throw new Error('response is missing tokens');
78
+ }
79
+
80
+ // If baseURL is provided, use it instead of request.nextUrl
81
+ // This is useful if the app is being run in a container like docker where
82
+ // the hostname can be different from the one in the request
83
+ const url = baseURL ? new URL(baseURL) : new URL(requestUrl.toString());
84
+
85
+ // Cleanup params
86
+ url.searchParams.delete('code');
87
+ url.searchParams.delete('state');
88
+
89
+ // Redirect to the requested path and store the session
90
+ const returnPathname = returnPathnameState ?? returnPathnameOption;
91
+
92
+ // Extract pathname and search params from returnPathname
93
+ const parsedReturnUrl = new URL(returnPathname, 'https://placeholder.com');
94
+ url.pathname = parsedReturnUrl.pathname;
95
+ url.search = parsedReturnUrl.search;
96
+
97
+ // Fall back to standard Response if NextResponse is not available.
98
+ // This is to support Next.js 13.
99
+ const response = redirectWithFallback(url.toString());
100
+ preventCaching(response.headers);
101
+
102
+ // Always delete the PKCE cookie after handling the callback, regardless of success or error
103
+ // to avoid stale cookies affecting future auth attempts & prevent replays
104
+ response.headers.append('Set-Cookie', `${pkceCookieName}=; ${getPKCECookieOptions(request.url, true, true)}`);
105
+
106
+ await saveSession({ accessToken, refreshToken, user, impersonator }, request);
107
+
108
+ if (onSuccess) {
109
+ await onSuccess({
110
+ accessToken,
111
+ refreshToken,
112
+ user,
113
+ impersonator,
114
+ oauthTokens,
115
+ authenticationMethod,
116
+ organizationId,
117
+ state: customState,
118
+ });
119
+ }
120
+
121
+ return response;
122
+ } catch (error) {
123
+ console.error('[AuthKit callback error]', error);
124
+ const response = await errorResponse(request, error);
125
+
126
+ // Always delete the PKCE cookie after handling the callback, regardless of success or error
127
+ // to avoid stale cookies affecting future auth attempts & prevent replays
128
+ if (state) {
129
+ response.headers.append(
130
+ 'Set-Cookie',
131
+ `${getPKCECookieNameForState(state)}=; ${getPKCECookieOptions(request.url, true, true)}`,
132
+ );
120
133
  }
121
- }
122
134
 
123
- return errorResponse(request);
135
+ return response;
136
+ }
124
137
  };
125
138
 
126
- function errorResponse(request: NextRequest, error?: unknown) {
139
+ async function errorResponse(request: NextRequest, error?: unknown) {
127
140
  if (onError) {
128
- return onError({ error, request });
141
+ const response = await onError({ error, request });
142
+ preventCaching(response.headers);
143
+ return response;
129
144
  }
130
145
 
131
- return errorResponseWithFallback({
146
+ const response = errorResponseWithFallback({
132
147
  error: {
133
148
  message: 'Something went wrong',
134
149
  description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.",
135
150
  },
136
151
  });
152
+
153
+ preventCaching(response.headers);
154
+ return response;
137
155
  }
138
156
  }