@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.
- package/README.md +124 -29
- package/dist/esm/auth.js +18 -5
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/components/tokenStore.js +110 -11
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +34 -4
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/cookie.js +51 -0
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/get-authorization-url.js +2 -1
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/middleware.js +2 -2
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/session.js +36 -3
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +57 -0
- package/dist/esm/test-helpers.js.map +1 -0
- package/dist/esm/types/auth.d.ts +5 -3
- package/dist/esm/types/components/tokenStore.d.ts +7 -2
- package/dist/esm/types/cookie.d.ts +1 -0
- package/dist/esm/types/interfaces.d.ts +3 -0
- package/dist/esm/types/middleware.d.ts +1 -1
- package/dist/esm/types/session.d.ts +2 -1
- package/dist/esm/types/test-helpers.d.ts +3 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/workos.js +1 -1
- package/package.json +5 -4
- package/src/actions.spec.ts +100 -0
- package/src/auth.spec.ts +347 -0
- package/src/auth.ts +19 -6
- package/src/authkit-callback-route.spec.ts +258 -0
- package/src/components/authkit-provider.spec.tsx +471 -0
- package/src/components/button.spec.tsx +46 -0
- package/src/components/impersonation.spec.tsx +134 -0
- package/src/components/min-max-button.spec.tsx +60 -0
- package/src/components/tokenStore.spec.ts +816 -0
- package/src/components/tokenStore.ts +147 -12
- package/src/components/useAccessToken.spec.tsx +731 -0
- package/src/components/useAccessToken.ts +40 -6
- package/src/components/useTokenClaims.spec.tsx +194 -0
- package/src/cookie.spec.ts +276 -0
- package/src/cookie.ts +56 -0
- package/src/get-authorization-url.spec.ts +60 -0
- package/src/get-authorization-url.ts +2 -0
- package/src/interfaces.ts +3 -0
- package/src/jwt.spec.ts +159 -0
- package/src/middleware.ts +2 -1
- package/src/session.spec.ts +1152 -0
- package/src/session.ts +42 -2
- package/src/test-helpers.ts +70 -0
- package/src/utils.spec.ts +142 -0
- package/src/workos.spec.ts +67 -0
- 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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
64
|
-
|
|
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;
|