antaeus.keycloak.react 2.6.1 → 2.6.4
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 +3 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +114 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +115 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ That's it! You now have full authentication with automatic token refresh.
|
|
|
30
30
|
- Zero-config setup
|
|
31
31
|
- Automatic token lifecycle management
|
|
32
32
|
- Token refresh on app startup
|
|
33
|
+
- Token refresh when suspended mobile/PWA sessions resume
|
|
33
34
|
- Silent token renewal
|
|
34
35
|
- Session monitoring UI
|
|
35
36
|
- Protected routes
|
|
@@ -152,6 +153,8 @@ const { isEnabled, isConfigured, reason } = useKeycloakStatus();
|
|
|
152
153
|
- **on-demand**: Only when API returns 401
|
|
153
154
|
- **both** (recommended): Startup + 401 retry
|
|
154
155
|
|
|
156
|
+
For mobile browsers and installed PWAs, token freshness is also checked when the app resumes from the background by default. Set `silentRenew.refreshOnResume` to `false` to disable this behavior.
|
|
157
|
+
|
|
155
158
|
## React Hooks
|
|
156
159
|
|
|
157
160
|
### useAuth()
|
package/dist/index.d.mts
CHANGED
|
@@ -121,6 +121,12 @@ interface SilentRenewConfig {
|
|
|
121
121
|
* @default true
|
|
122
122
|
*/
|
|
123
123
|
useRefreshToken?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Re-check token freshness when a suspended tab/PWA resumes.
|
|
126
|
+
* Useful for mobile browsers and installed PWAs where timers pause in the background.
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
129
|
+
refreshOnResume?: boolean;
|
|
124
130
|
}
|
|
125
131
|
/**
|
|
126
132
|
* Event callbacks for authentication lifecycle
|
package/dist/index.d.ts
CHANGED
|
@@ -121,6 +121,12 @@ interface SilentRenewConfig {
|
|
|
121
121
|
* @default true
|
|
122
122
|
*/
|
|
123
123
|
useRefreshToken?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Re-check token freshness when a suspended tab/PWA resumes.
|
|
126
|
+
* Useful for mobile browsers and installed PWAs where timers pause in the background.
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
129
|
+
refreshOnResume?: boolean;
|
|
124
130
|
}
|
|
125
131
|
/**
|
|
126
132
|
* Event callbacks for authentication lifecycle
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,8 @@ var defaultConfig = {
|
|
|
36
36
|
// 30 days in seconds
|
|
37
37
|
showLoadingIndicator: false,
|
|
38
38
|
loadingMessage: "Refreshing session...",
|
|
39
|
-
useRefreshToken: true
|
|
39
|
+
useRefreshToken: true,
|
|
40
|
+
refreshOnResume: true
|
|
40
41
|
},
|
|
41
42
|
advanced: {
|
|
42
43
|
responseType: "code",
|
|
@@ -165,6 +166,20 @@ var isTransientNetworkError = (error) => {
|
|
|
165
166
|
}
|
|
166
167
|
return message.includes("network") || message.includes("failed to fetch") || message.includes("fetch") || message.includes("timeout") || message.includes("temporarily unavailable") || message.includes("dns") || message.includes("offline");
|
|
167
168
|
};
|
|
169
|
+
var isInvalidRefreshTokenError = (error) => {
|
|
170
|
+
if (error instanceof oidcClientTs.ErrorResponse) {
|
|
171
|
+
const code = error.error?.toLowerCase?.();
|
|
172
|
+
if (code === "invalid_grant" || code === "invalid_token") {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
const description = error.error_description?.toLowerCase?.();
|
|
176
|
+
if (description?.includes("refresh token")) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const message = error.message?.toLowerCase?.() ?? "";
|
|
181
|
+
return message.includes("invalid_grant") || message.includes("refresh token") || message.includes("invalid token");
|
|
182
|
+
};
|
|
168
183
|
function useTokenLifecycle(options) {
|
|
169
184
|
const {
|
|
170
185
|
config,
|
|
@@ -177,6 +192,7 @@ function useTokenLifecycle(options) {
|
|
|
177
192
|
const auth = useAuth();
|
|
178
193
|
const [isRefreshing, setIsRefreshing] = react.useState(false);
|
|
179
194
|
const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
|
|
195
|
+
const isRefreshingRef = react.useRef(false);
|
|
180
196
|
const hasCheckedOnStartup = react.useRef(false);
|
|
181
197
|
const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
|
|
182
198
|
const [pendingRetry, setPendingRetry] = react.useState(null);
|
|
@@ -229,10 +245,11 @@ function useTokenLifecycle(options) {
|
|
|
229
245
|
return elapsedSeconds >= maxLifetime;
|
|
230
246
|
}, [config.silentRenew?.maxRefreshTokenLifetime, getStorageKey, log, storeFirstLoginTime]);
|
|
231
247
|
const refreshToken = react.useCallback(async () => {
|
|
232
|
-
if (
|
|
248
|
+
if (isRefreshingRef.current) {
|
|
233
249
|
log("Refresh already in progress, skipping");
|
|
234
250
|
return;
|
|
235
251
|
}
|
|
252
|
+
isRefreshingRef.current = true;
|
|
236
253
|
setIsRefreshing(true);
|
|
237
254
|
deferredRefreshReasonRef.current = null;
|
|
238
255
|
try {
|
|
@@ -255,6 +272,20 @@ function useTokenLifecycle(options) {
|
|
|
255
272
|
onRefreshSuccess?.();
|
|
256
273
|
} catch (error) {
|
|
257
274
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
275
|
+
if (isInvalidRefreshTokenError(err)) {
|
|
276
|
+
log("Refresh token rejected by the identity provider, clearing stored session");
|
|
277
|
+
setIsRefreshTokenExpired(true);
|
|
278
|
+
onRefreshTokenExpired?.();
|
|
279
|
+
const key = getStorageKey();
|
|
280
|
+
localStorage.removeItem(key);
|
|
281
|
+
try {
|
|
282
|
+
await auth.removeUser();
|
|
283
|
+
} catch (removeError) {
|
|
284
|
+
log("Error removing user after invalid refresh token:", removeError);
|
|
285
|
+
}
|
|
286
|
+
onRefreshError?.(err);
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
258
289
|
if (isTransientNetworkError(err)) {
|
|
259
290
|
const reason = isNavigatorOffline() ? "offline" : "network";
|
|
260
291
|
deferredRefreshReasonRef.current = reason;
|
|
@@ -274,10 +305,10 @@ function useTokenLifecycle(options) {
|
|
|
274
305
|
onRefreshError?.(err);
|
|
275
306
|
throw err;
|
|
276
307
|
} finally {
|
|
308
|
+
isRefreshingRef.current = false;
|
|
277
309
|
setIsRefreshing(false);
|
|
278
310
|
}
|
|
279
311
|
}, [
|
|
280
|
-
isRefreshing,
|
|
281
312
|
auth,
|
|
282
313
|
checkRefreshTokenLifetime,
|
|
283
314
|
onRefreshStart,
|
|
@@ -285,6 +316,39 @@ function useTokenLifecycle(options) {
|
|
|
285
316
|
onRefreshError,
|
|
286
317
|
onRefreshTokenExpired,
|
|
287
318
|
enqueueRetry,
|
|
319
|
+
getStorageKey,
|
|
320
|
+
log
|
|
321
|
+
]);
|
|
322
|
+
const shouldRefreshOnResume = react.useCallback((reason) => {
|
|
323
|
+
const silentRenewEnabled = config.silentRenew?.enabled !== false;
|
|
324
|
+
const refreshOnResume = config.silentRenew?.refreshOnResume !== false;
|
|
325
|
+
const strategy = config.silentRenew?.refreshStrategy || "both";
|
|
326
|
+
const strategyAllowsResume = strategy === "startup" || strategy === "both";
|
|
327
|
+
const hasRefreshToken = Boolean(auth.user?.refresh_token);
|
|
328
|
+
if (!silentRenewEnabled || !refreshOnResume || !strategyAllowsResume) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (auth.isLoading) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (!auth.user) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (supportsRefreshTokens && !hasRefreshToken) {
|
|
338
|
+
log("Skipping resume token check because no refresh token is available:", reason);
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
if (reason !== "online" && typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}, [
|
|
346
|
+
auth.isLoading,
|
|
347
|
+
auth.user,
|
|
348
|
+
config.silentRenew?.enabled,
|
|
349
|
+
config.silentRenew?.refreshOnResume,
|
|
350
|
+
config.silentRenew?.refreshStrategy,
|
|
351
|
+
supportsRefreshTokens,
|
|
288
352
|
log
|
|
289
353
|
]);
|
|
290
354
|
const checkAndRefreshTokens = react.useCallback(async () => {
|
|
@@ -434,6 +498,38 @@ function useTokenLifecycle(options) {
|
|
|
434
498
|
checkAndRefreshTokens,
|
|
435
499
|
log
|
|
436
500
|
]);
|
|
501
|
+
react.useEffect(() => {
|
|
502
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const runResumeCheck = (reason) => {
|
|
506
|
+
if (!shouldRefreshOnResume(reason)) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
log("Running resume token check after:", reason);
|
|
510
|
+
checkAndRefreshTokens().catch((error) => {
|
|
511
|
+
log("Resume token check failed:", error);
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
const handleVisibilityChange = () => {
|
|
515
|
+
if (document.visibilityState === "visible") {
|
|
516
|
+
runResumeCheck("visibilitychange");
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const handlePageShow = () => runResumeCheck("pageshow");
|
|
520
|
+
const handleFocus = () => runResumeCheck("focus");
|
|
521
|
+
const handleOnline = () => runResumeCheck("online");
|
|
522
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
523
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
524
|
+
window.addEventListener("focus", handleFocus);
|
|
525
|
+
window.addEventListener("online", handleOnline);
|
|
526
|
+
return () => {
|
|
527
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
528
|
+
window.removeEventListener("pageshow", handlePageShow);
|
|
529
|
+
window.removeEventListener("focus", handleFocus);
|
|
530
|
+
window.removeEventListener("online", handleOnline);
|
|
531
|
+
};
|
|
532
|
+
}, [checkAndRefreshTokens, log, shouldRefreshOnResume]);
|
|
437
533
|
react.useEffect(() => {
|
|
438
534
|
if (hasCheckedOnStartup.current) {
|
|
439
535
|
const hasUser = Boolean(auth.user);
|
|
@@ -448,6 +544,11 @@ function useTokenLifecycle(options) {
|
|
|
448
544
|
storeFirstLoginTime();
|
|
449
545
|
}
|
|
450
546
|
}, [auth.user, auth.isLoading, storeFirstLoginTime]);
|
|
547
|
+
react.useEffect(() => {
|
|
548
|
+
if (auth.isAuthenticated && isRefreshTokenExpired) {
|
|
549
|
+
setIsRefreshTokenExpired(false);
|
|
550
|
+
}
|
|
551
|
+
}, [auth.isAuthenticated, isRefreshTokenExpired]);
|
|
451
552
|
react.useEffect(() => {
|
|
452
553
|
if (!auth.isAuthenticated || auth.isLoading) {
|
|
453
554
|
return;
|
|
@@ -851,7 +952,8 @@ var CONFIG_PRESETS = {
|
|
|
851
952
|
maxRefreshTokenLifetime: 2592e3,
|
|
852
953
|
// 30 days
|
|
853
954
|
showLoadingIndicator: false,
|
|
854
|
-
loadingMessage: "Refreshing session..."
|
|
955
|
+
loadingMessage: "Refreshing session...",
|
|
956
|
+
refreshOnResume: true
|
|
855
957
|
},
|
|
856
958
|
advanced: {
|
|
857
959
|
responseType: "code",
|
|
@@ -887,7 +989,8 @@ var CONFIG_PRESETS = {
|
|
|
887
989
|
// 30 days
|
|
888
990
|
showLoadingIndicator: true,
|
|
889
991
|
// Show loading for better UX
|
|
890
|
-
loadingMessage: "Restoring your session..."
|
|
992
|
+
loadingMessage: "Restoring your session...",
|
|
993
|
+
refreshOnResume: true
|
|
891
994
|
},
|
|
892
995
|
advanced: {
|
|
893
996
|
responseType: "code",
|
|
@@ -924,7 +1027,8 @@ var CONFIG_PRESETS = {
|
|
|
924
1027
|
// 24 hours only
|
|
925
1028
|
showLoadingIndicator: false,
|
|
926
1029
|
// Silent for security
|
|
927
|
-
loadingMessage: "Validating session..."
|
|
1030
|
+
loadingMessage: "Validating session...",
|
|
1031
|
+
refreshOnResume: true
|
|
928
1032
|
},
|
|
929
1033
|
advanced: {
|
|
930
1034
|
responseType: "code",
|
|
@@ -958,7 +1062,8 @@ var CONFIG_PRESETS = {
|
|
|
958
1062
|
maxRefreshTokenLifetime: 604800,
|
|
959
1063
|
// 7 days
|
|
960
1064
|
showLoadingIndicator: false,
|
|
961
|
-
loadingMessage: "Refreshing session..."
|
|
1065
|
+
loadingMessage: "Refreshing session...",
|
|
1066
|
+
refreshOnResume: true
|
|
962
1067
|
},
|
|
963
1068
|
advanced: {
|
|
964
1069
|
responseType: "code",
|
|
@@ -995,7 +1100,8 @@ var CONFIG_PRESETS = {
|
|
|
995
1100
|
// No limit (for convenience)
|
|
996
1101
|
showLoadingIndicator: true,
|
|
997
1102
|
// Show for debugging
|
|
998
|
-
loadingMessage: "[DEV] Refreshing tokens..."
|
|
1103
|
+
loadingMessage: "[DEV] Refreshing tokens...",
|
|
1104
|
+
refreshOnResume: true
|
|
999
1105
|
},
|
|
1000
1106
|
advanced: {
|
|
1001
1107
|
responseType: "code",
|