@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.
Files changed (106) hide show
  1. package/README.md +276 -102
  2. package/dist/esm/actions.js +35 -4
  3. package/dist/esm/actions.js.map +1 -1
  4. package/dist/esm/auth.js +51 -20
  5. package/dist/esm/auth.js.map +1 -1
  6. package/dist/esm/authkit-callback-route.js +82 -93
  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 +16 -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 +38 -0
  37. package/dist/esm/pkce.js.map +1 -0
  38. package/dist/esm/session.js +73 -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 +8 -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 +12 -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 +32 -34
  66. package/src/actions.spec.ts +94 -17
  67. package/src/actions.ts +44 -5
  68. package/src/auth.spec.ts +60 -29
  69. package/src/auth.ts +55 -41
  70. package/src/authkit-callback-route.spec.ts +310 -58
  71. package/src/authkit-callback-route.ts +106 -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 +14 -9
  83. package/src/cookie.ts +29 -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 +125 -0
  97. package/src/pkce.ts +42 -0
  98. package/src/session.spec.ts +87 -89
  99. package/src/session.ts +91 -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, 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,117 @@ 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
+ // Always delete the PKCE cookie after handling the callback, regardless of success or error
29
+ // to avoid stale cookies affecting future auth attempts & prevent replays
30
+ const deleteCookie = `${PKCE_COOKIE_NAME}=; ${getPKCECookieOptions(request.url, true, true)}`;
31
+
32
+ // We want to catch any & all errors and respond the same way
33
+ // Firstly, by destroying the 1-use PKCE cookie to prevent replay attacks
34
+ // or stale cookies affecting future auth attempts
35
+ try {
36
+ // Fall back to standard URL parsing when nextUrl is not available (e.g., vinext)
37
+ const requestUrl = request.nextUrl ?? new URL(request.url);
38
+
39
+ // Gather mandatory information
40
+ const code = requestUrl.searchParams.get('code');
41
+ const state = requestUrl.searchParams.get('state');
42
+ const pkceCookie = request.cookies.get(PKCE_COOKIE_NAME)?.value;
43
+
44
+ if (!code || !state) {
45
+ throw new Error('Missing required auth parameter');
120
46
  }
121
- }
122
47
 
123
- return errorResponse(request);
48
+ // CSRF verification: both channels (cookie + URL state) must be present and match
49
+ if (!pkceCookie) {
50
+ throw new Error(
51
+ 'Auth cookie missing — cannot verify OAuth state. Ensure Set-Cookie headers are propagated on redirects.',
52
+ );
53
+ }
54
+
55
+ if (state !== pkceCookie) {
56
+ throw new Error('OAuth state mismatch');
57
+ }
58
+
59
+ const {
60
+ codeVerifier,
61
+ customState,
62
+ returnPathname: returnPathnameState,
63
+ } = await getStateFromPKCECookieValue(pkceCookie);
64
+
65
+ // Use the code returned to us by AuthKit and authenticate the user with WorkOS
66
+ const { accessToken, refreshToken, user, impersonator, oauthTokens, authenticationMethod, organizationId } =
67
+ await getWorkOS().userManagement.authenticateWithCode({
68
+ clientId: WORKOS_CLIENT_ID,
69
+ code,
70
+ codeVerifier,
71
+ });
72
+
73
+ if (!accessToken || !refreshToken) {
74
+ throw new Error('response is missing tokens');
75
+ }
76
+
77
+ // If baseURL is provided, use it instead of request.nextUrl
78
+ // This is useful if the app is being run in a container like docker where
79
+ // the hostname can be different from the one in the request
80
+ const url = baseURL ? new URL(baseURL) : new URL(requestUrl.toString());
81
+
82
+ // Cleanup params
83
+ url.searchParams.delete('code');
84
+ url.searchParams.delete('state');
85
+
86
+ // Redirect to the requested path and store the session
87
+ const returnPathname = returnPathnameState ?? returnPathnameOption;
88
+
89
+ // Extract pathname and search params from returnPathname
90
+ const parsedReturnUrl = new URL(returnPathname, 'https://placeholder.com');
91
+ url.pathname = parsedReturnUrl.pathname;
92
+ url.search = parsedReturnUrl.search;
93
+
94
+ // Fall back to standard Response if NextResponse is not available.
95
+ // This is to support Next.js 13.
96
+ const response = redirectWithFallback(url.toString());
97
+ preventCaching(response.headers);
98
+ response.headers.append('Set-Cookie', deleteCookie);
99
+
100
+ await saveSession({ accessToken, refreshToken, user, impersonator }, request);
101
+
102
+ if (onSuccess) {
103
+ await onSuccess({
104
+ accessToken,
105
+ refreshToken,
106
+ user,
107
+ impersonator,
108
+ oauthTokens,
109
+ authenticationMethod,
110
+ organizationId,
111
+ state: customState,
112
+ });
113
+ }
114
+
115
+ return response;
116
+ } catch (error) {
117
+ console.error('[AuthKit callback error]', error);
118
+ const response = await errorResponse(request, error);
119
+ response.headers.append('Set-Cookie', deleteCookie);
120
+ return response;
121
+ }
124
122
  };
125
123
 
126
- function errorResponse(request: NextRequest, error?: unknown) {
124
+ async function errorResponse(request: NextRequest, error?: unknown) {
127
125
  if (onError) {
128
- return onError({ error, request });
126
+ const response = await onError({ error, request });
127
+ preventCaching(response.headers);
128
+ return response;
129
129
  }
130
130
 
131
- return errorResponseWithFallback({
131
+ const response = errorResponseWithFallback({
132
132
  error: {
133
133
  message: 'Something went wrong',
134
134
  description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.",
135
135
  },
136
136
  });
137
+
138
+ preventCaching(response.headers);
139
+ return response;
137
140
  }
138
141
  }