@workos-inc/authkit-nextjs 2.5.0 → 2.7.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 (53) hide show
  1. package/README.md +124 -29
  2. package/dist/esm/auth.js +18 -5
  3. package/dist/esm/auth.js.map +1 -1
  4. package/dist/esm/components/tokenStore.js +110 -11
  5. package/dist/esm/components/tokenStore.js.map +1 -1
  6. package/dist/esm/components/useAccessToken.js +34 -4
  7. package/dist/esm/components/useAccessToken.js.map +1 -1
  8. package/dist/esm/cookie.js +51 -0
  9. package/dist/esm/cookie.js.map +1 -1
  10. package/dist/esm/get-authorization-url.js +2 -1
  11. package/dist/esm/get-authorization-url.js.map +1 -1
  12. package/dist/esm/middleware.js +2 -2
  13. package/dist/esm/middleware.js.map +1 -1
  14. package/dist/esm/session.js +36 -3
  15. package/dist/esm/session.js.map +1 -1
  16. package/dist/esm/test-helpers.js +57 -0
  17. package/dist/esm/test-helpers.js.map +1 -0
  18. package/dist/esm/types/auth.d.ts +5 -3
  19. package/dist/esm/types/components/tokenStore.d.ts +7 -2
  20. package/dist/esm/types/cookie.d.ts +1 -0
  21. package/dist/esm/types/interfaces.d.ts +3 -0
  22. package/dist/esm/types/middleware.d.ts +1 -1
  23. package/dist/esm/types/session.d.ts +2 -1
  24. package/dist/esm/types/test-helpers.d.ts +3 -0
  25. package/dist/esm/types/workos.d.ts +1 -1
  26. package/dist/esm/workos.js +1 -1
  27. package/package.json +5 -4
  28. package/src/actions.spec.ts +100 -0
  29. package/src/auth.spec.ts +347 -0
  30. package/src/auth.ts +19 -6
  31. package/src/authkit-callback-route.spec.ts +258 -0
  32. package/src/components/authkit-provider.spec.tsx +471 -0
  33. package/src/components/button.spec.tsx +46 -0
  34. package/src/components/impersonation.spec.tsx +134 -0
  35. package/src/components/min-max-button.spec.tsx +60 -0
  36. package/src/components/tokenStore.spec.ts +816 -0
  37. package/src/components/tokenStore.ts +147 -12
  38. package/src/components/useAccessToken.spec.tsx +731 -0
  39. package/src/components/useAccessToken.ts +40 -6
  40. package/src/components/useTokenClaims.spec.tsx +194 -0
  41. package/src/cookie.spec.ts +276 -0
  42. package/src/cookie.ts +56 -0
  43. package/src/get-authorization-url.spec.ts +60 -0
  44. package/src/get-authorization-url.ts +2 -0
  45. package/src/interfaces.ts +3 -0
  46. package/src/jwt.spec.ts +159 -0
  47. package/src/middleware.ts +2 -1
  48. package/src/session.spec.ts +1152 -0
  49. package/src/session.ts +42 -2
  50. package/src/test-helpers.ts +70 -0
  51. package/src/utils.spec.ts +142 -0
  52. package/src/workos.spec.ts +67 -0
  53. package/src/workos.ts +1 -1
@@ -11,19 +11,44 @@ const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
11
11
  const MIN_REFRESH_DELAY_SECONDS = 15;
12
12
  const MAX_REFRESH_DELAY_SECONDS = 24 * 60 * 60;
13
13
  const RETRY_DELAY_SECONDS = 300; // 5 minutes for retry on error
14
+ const jwtCookieName = 'workos-access-token';
15
+
16
+ export class TokenStore {
17
+ private state: TokenState;
18
+ private serverSnapshot: TokenState;
19
+
20
+ constructor() {
21
+ // Initialize state with token from cookie if available
22
+ const initialToken = this.getInitialTokenFromCookie();
23
+ this.state = {
24
+ token: initialToken,
25
+ loading: false,
26
+ error: null,
27
+ };
14
28
 
15
- class TokenStore {
16
- private static readonly SERVER_SNAPSHOT: TokenState = { token: undefined, loading: false, error: null };
29
+ // Server snapshot should match initial state for hydration
30
+ this.serverSnapshot = {
31
+ token: initialToken,
32
+ loading: false,
33
+ error: null,
34
+ };
17
35
 
18
- private state: TokenState = {
19
- token: undefined,
20
- loading: false,
21
- error: null,
22
- };
36
+ /* istanbul ignore next */
37
+ if (initialToken) {
38
+ // Mark as consumed if we found a token
39
+ this.fastCookieConsumed = true;
40
+ // Schedule refresh based on token expiry
41
+ const tokenData = this.parseToken(initialToken);
42
+ if (tokenData) {
43
+ this.scheduleRefresh(tokenData.timeUntilExpiry);
44
+ }
45
+ }
46
+ }
23
47
 
24
48
  private listeners = new Set<() => void>();
25
49
  private refreshPromise: Promise<string | undefined> | null = null;
26
50
  private refreshTimeout: ReturnType<typeof setTimeout> | undefined;
51
+ private fastCookieConsumed = false;
27
52
 
28
53
  subscribe = (listener: () => void) => {
29
54
  this.listeners.add(listener);
@@ -38,7 +63,7 @@ class TokenStore {
38
63
 
39
64
  getSnapshot = () => this.state;
40
65
 
41
- getServerSnapshot = () => TokenStore.SERVER_SNAPSHOT;
66
+ getServerSnapshot = () => this.serverSnapshot;
42
67
 
43
68
  private notify() {
44
69
  this.listeners.forEach((listener) => listener());
@@ -58,10 +83,12 @@ class TokenStore {
58
83
  const delay =
59
84
  typeof timeUntilExpiry === 'undefined' ? RETRY_DELAY_SECONDS * 1000 : this.getRefreshDelay(timeUntilExpiry);
60
85
 
61
- this.refreshTimeout = setTimeout(() => {
62
- /* istanbul ignore next */
63
- void this.getAccessTokenSilently().catch(() => {});
64
- }, delay);
86
+ this.refreshTimeout = setTimeout(
87
+ /* istanbul ignore next */ () => {
88
+ void this.getAccessTokenSilently().catch(/* istanbul ignore next */ () => {});
89
+ },
90
+ delay,
91
+ );
65
92
  }
66
93
 
67
94
  private getRefreshDelay(timeUntilExpiry: number) {
@@ -74,6 +101,92 @@ class TokenStore {
74
101
  return Math.min(Math.max(idealDelay, MIN_REFRESH_DELAY_SECONDS * 1000), MAX_REFRESH_DELAY_SECONDS * 1000);
75
102
  }
76
103
 
104
+ private deleteCookie() {
105
+ const isSecure = window.location.protocol === 'https:';
106
+
107
+ // Build deletion string to match EXACTLY what the server sets
108
+ // Server sets: Path=/, SameSite=Lax, and Secure (if HTTPS)
109
+ // NO Domain attribute is set by server, so we don't set it either
110
+ const deletionString = isSecure
111
+ ? `${jwtCookieName}=; SameSite=Lax; Max-Age=0; Secure`
112
+ : `${jwtCookieName}=; SameSite=Lax; Max-Age=0`;
113
+
114
+ document.cookie = deletionString;
115
+
116
+ // The cookie might still appear in document.cookie even after deletion
117
+ // due to browser caching, but it should be expired and not sent to server
118
+ }
119
+
120
+ private getInitialTokenFromCookie(): string | undefined {
121
+ if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
122
+ return;
123
+ }
124
+
125
+ // Parse cookies without regex
126
+ const cookies = document.cookie.split(';').reduce(
127
+ (acc, cookie) => {
128
+ const [name, ...valueParts] = cookie.trim().split('=');
129
+ if (name && valueParts.length > 0) {
130
+ const value = valueParts.join('='); // Handle values that contain '='
131
+ acc[name.trim()] = decodeURIComponent(value);
132
+ }
133
+ return acc;
134
+ },
135
+ {} as Record<string, string>,
136
+ );
137
+
138
+ const token = cookies[jwtCookieName];
139
+ if (!token) {
140
+ return;
141
+ }
142
+
143
+ // Delete the cookie immediately after reading it
144
+ this.deleteCookie();
145
+
146
+ return token;
147
+ }
148
+
149
+ private consumeFastCookie(): string | undefined {
150
+ // Only try to consume once per page load
151
+ if (this.fastCookieConsumed) {
152
+ return;
153
+ }
154
+
155
+ if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
156
+ return;
157
+ }
158
+
159
+ // Parse cookies without regex
160
+ const cookies = document.cookie.split(';').reduce(
161
+ (acc, cookie) => {
162
+ const [name, ...valueParts] = cookie.trim().split('=');
163
+ if (name && valueParts.length > 0) {
164
+ const value = valueParts.join('='); // Handle values that contain '='
165
+ acc[name.trim()] = decodeURIComponent(value);
166
+ }
167
+ return acc;
168
+ },
169
+ {} as Record<string, string>,
170
+ );
171
+
172
+ const newToken = cookies[jwtCookieName];
173
+ if (!newToken) {
174
+ // Mark as consumed even if not found, to avoid repeated checks
175
+ this.fastCookieConsumed = true;
176
+ return;
177
+ }
178
+
179
+ // Mark as consumed BEFORE deleting to prevent race conditions
180
+ this.fastCookieConsumed = true;
181
+
182
+ // Delete the cookie using protocol-aware deletion
183
+ this.deleteCookie();
184
+
185
+ if (newToken !== this.state.token) {
186
+ return newToken;
187
+ }
188
+ }
189
+
77
190
  parseToken(token: string | undefined) {
78
191
  if (!token) return null;
79
192
 
@@ -123,6 +236,13 @@ class TokenStore {
123
236
  }
124
237
 
125
238
  async getAccessToken(): Promise<string | undefined> {
239
+ const fastToken = this.consumeFastCookie();
240
+
241
+ if (fastToken) {
242
+ this.setState({ token: fastToken, loading: false, error: null });
243
+ return fastToken;
244
+ }
245
+
126
246
  const tokenData = this.parseToken(this.state.token);
127
247
 
128
248
  // If we have a valid JWT that's not expiring, return it
@@ -140,6 +260,20 @@ class TokenStore {
140
260
  }
141
261
 
142
262
  async getAccessTokenSilently(): Promise<string | undefined> {
263
+ const fastToken = this.consumeFastCookie();
264
+
265
+ if (fastToken) {
266
+ this.setState({ token: fastToken, loading: false, error: null });
267
+
268
+ // Schedule refresh based on token expiry
269
+ const tokenData = this.parseToken(fastToken);
270
+ if (tokenData) {
271
+ this.scheduleRefresh(tokenData.timeUntilExpiry);
272
+ }
273
+
274
+ return fastToken;
275
+ }
276
+
143
277
  const tokenData = this.parseToken(this.state.token);
144
278
 
145
279
  // If we have a valid JWT that's not expiring, return it
@@ -257,6 +391,7 @@ class TokenStore {
257
391
  reset() {
258
392
  this.state = { token: undefined, loading: false, error: null };
259
393
  this.refreshPromise = null;
394
+ this.fastCookieConsumed = false;
260
395
  if (this.refreshTimeout) {
261
396
  clearTimeout(this.refreshTimeout);
262
397
  this.refreshTimeout = undefined;