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 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 (isRefreshing) {
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",