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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
2
2
  import { useAuth as useAuth$1, AuthContext, AuthProvider } from 'react-oidc-context';
3
- import { WebStorageStateStore, User } from 'oidc-client-ts';
3
+ import { WebStorageStateStore, User, ErrorResponse } from 'oidc-client-ts';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { useLocation } from 'react-router-dom';
6
6
  import { QRCodeSVG } from 'qrcode.react';
@@ -33,7 +33,8 @@ var defaultConfig = {
33
33
  // 30 days in seconds
34
34
  showLoadingIndicator: false,
35
35
  loadingMessage: "Refreshing session...",
36
- useRefreshToken: true
36
+ useRefreshToken: true,
37
+ refreshOnResume: true
37
38
  },
38
39
  advanced: {
39
40
  responseType: "code",
@@ -162,6 +163,20 @@ var isTransientNetworkError = (error) => {
162
163
  }
163
164
  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");
164
165
  };
166
+ var isInvalidRefreshTokenError = (error) => {
167
+ if (error instanceof ErrorResponse) {
168
+ const code = error.error?.toLowerCase?.();
169
+ if (code === "invalid_grant" || code === "invalid_token") {
170
+ return true;
171
+ }
172
+ const description = error.error_description?.toLowerCase?.();
173
+ if (description?.includes("refresh token")) {
174
+ return true;
175
+ }
176
+ }
177
+ const message = error.message?.toLowerCase?.() ?? "";
178
+ return message.includes("invalid_grant") || message.includes("refresh token") || message.includes("invalid token");
179
+ };
165
180
  function useTokenLifecycle(options) {
166
181
  const {
167
182
  config,
@@ -174,6 +189,7 @@ function useTokenLifecycle(options) {
174
189
  const auth = useAuth();
175
190
  const [isRefreshing, setIsRefreshing] = useState(false);
176
191
  const [isRefreshTokenExpired, setIsRefreshTokenExpired] = useState(false);
192
+ const isRefreshingRef = useRef(false);
177
193
  const hasCheckedOnStartup = useRef(false);
178
194
  const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
179
195
  const [pendingRetry, setPendingRetry] = useState(null);
@@ -226,10 +242,11 @@ function useTokenLifecycle(options) {
226
242
  return elapsedSeconds >= maxLifetime;
227
243
  }, [config.silentRenew?.maxRefreshTokenLifetime, getStorageKey, log, storeFirstLoginTime]);
228
244
  const refreshToken = useCallback(async () => {
229
- if (isRefreshing) {
245
+ if (isRefreshingRef.current) {
230
246
  log("Refresh already in progress, skipping");
231
247
  return;
232
248
  }
249
+ isRefreshingRef.current = true;
233
250
  setIsRefreshing(true);
234
251
  deferredRefreshReasonRef.current = null;
235
252
  try {
@@ -252,6 +269,20 @@ function useTokenLifecycle(options) {
252
269
  onRefreshSuccess?.();
253
270
  } catch (error) {
254
271
  const err = error instanceof Error ? error : new Error(String(error));
272
+ if (isInvalidRefreshTokenError(err)) {
273
+ log("Refresh token rejected by the identity provider, clearing stored session");
274
+ setIsRefreshTokenExpired(true);
275
+ onRefreshTokenExpired?.();
276
+ const key = getStorageKey();
277
+ localStorage.removeItem(key);
278
+ try {
279
+ await auth.removeUser();
280
+ } catch (removeError) {
281
+ log("Error removing user after invalid refresh token:", removeError);
282
+ }
283
+ onRefreshError?.(err);
284
+ throw err;
285
+ }
255
286
  if (isTransientNetworkError(err)) {
256
287
  const reason = isNavigatorOffline() ? "offline" : "network";
257
288
  deferredRefreshReasonRef.current = reason;
@@ -271,10 +302,10 @@ function useTokenLifecycle(options) {
271
302
  onRefreshError?.(err);
272
303
  throw err;
273
304
  } finally {
305
+ isRefreshingRef.current = false;
274
306
  setIsRefreshing(false);
275
307
  }
276
308
  }, [
277
- isRefreshing,
278
309
  auth,
279
310
  checkRefreshTokenLifetime,
280
311
  onRefreshStart,
@@ -282,6 +313,39 @@ function useTokenLifecycle(options) {
282
313
  onRefreshError,
283
314
  onRefreshTokenExpired,
284
315
  enqueueRetry,
316
+ getStorageKey,
317
+ log
318
+ ]);
319
+ const shouldRefreshOnResume = useCallback((reason) => {
320
+ const silentRenewEnabled = config.silentRenew?.enabled !== false;
321
+ const refreshOnResume = config.silentRenew?.refreshOnResume !== false;
322
+ const strategy = config.silentRenew?.refreshStrategy || "both";
323
+ const strategyAllowsResume = strategy === "startup" || strategy === "both";
324
+ const hasRefreshToken = Boolean(auth.user?.refresh_token);
325
+ if (!silentRenewEnabled || !refreshOnResume || !strategyAllowsResume) {
326
+ return false;
327
+ }
328
+ if (auth.isLoading) {
329
+ return false;
330
+ }
331
+ if (!auth.user) {
332
+ return false;
333
+ }
334
+ if (supportsRefreshTokens && !hasRefreshToken) {
335
+ log("Skipping resume token check because no refresh token is available:", reason);
336
+ return false;
337
+ }
338
+ if (reason !== "online" && typeof document !== "undefined" && document.visibilityState === "hidden") {
339
+ return false;
340
+ }
341
+ return true;
342
+ }, [
343
+ auth.isLoading,
344
+ auth.user,
345
+ config.silentRenew?.enabled,
346
+ config.silentRenew?.refreshOnResume,
347
+ config.silentRenew?.refreshStrategy,
348
+ supportsRefreshTokens,
285
349
  log
286
350
  ]);
287
351
  const checkAndRefreshTokens = useCallback(async () => {
@@ -431,6 +495,38 @@ function useTokenLifecycle(options) {
431
495
  checkAndRefreshTokens,
432
496
  log
433
497
  ]);
498
+ useEffect(() => {
499
+ if (typeof window === "undefined" || typeof document === "undefined") {
500
+ return;
501
+ }
502
+ const runResumeCheck = (reason) => {
503
+ if (!shouldRefreshOnResume(reason)) {
504
+ return;
505
+ }
506
+ log("Running resume token check after:", reason);
507
+ checkAndRefreshTokens().catch((error) => {
508
+ log("Resume token check failed:", error);
509
+ });
510
+ };
511
+ const handleVisibilityChange = () => {
512
+ if (document.visibilityState === "visible") {
513
+ runResumeCheck("visibilitychange");
514
+ }
515
+ };
516
+ const handlePageShow = () => runResumeCheck("pageshow");
517
+ const handleFocus = () => runResumeCheck("focus");
518
+ const handleOnline = () => runResumeCheck("online");
519
+ document.addEventListener("visibilitychange", handleVisibilityChange);
520
+ window.addEventListener("pageshow", handlePageShow);
521
+ window.addEventListener("focus", handleFocus);
522
+ window.addEventListener("online", handleOnline);
523
+ return () => {
524
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
525
+ window.removeEventListener("pageshow", handlePageShow);
526
+ window.removeEventListener("focus", handleFocus);
527
+ window.removeEventListener("online", handleOnline);
528
+ };
529
+ }, [checkAndRefreshTokens, log, shouldRefreshOnResume]);
434
530
  useEffect(() => {
435
531
  if (hasCheckedOnStartup.current) {
436
532
  const hasUser = Boolean(auth.user);
@@ -445,6 +541,11 @@ function useTokenLifecycle(options) {
445
541
  storeFirstLoginTime();
446
542
  }
447
543
  }, [auth.user, auth.isLoading, storeFirstLoginTime]);
544
+ useEffect(() => {
545
+ if (auth.isAuthenticated && isRefreshTokenExpired) {
546
+ setIsRefreshTokenExpired(false);
547
+ }
548
+ }, [auth.isAuthenticated, isRefreshTokenExpired]);
448
549
  useEffect(() => {
449
550
  if (!auth.isAuthenticated || auth.isLoading) {
450
551
  return;
@@ -848,7 +949,8 @@ var CONFIG_PRESETS = {
848
949
  maxRefreshTokenLifetime: 2592e3,
849
950
  // 30 days
850
951
  showLoadingIndicator: false,
851
- loadingMessage: "Refreshing session..."
952
+ loadingMessage: "Refreshing session...",
953
+ refreshOnResume: true
852
954
  },
853
955
  advanced: {
854
956
  responseType: "code",
@@ -884,7 +986,8 @@ var CONFIG_PRESETS = {
884
986
  // 30 days
885
987
  showLoadingIndicator: true,
886
988
  // Show loading for better UX
887
- loadingMessage: "Restoring your session..."
989
+ loadingMessage: "Restoring your session...",
990
+ refreshOnResume: true
888
991
  },
889
992
  advanced: {
890
993
  responseType: "code",
@@ -921,7 +1024,8 @@ var CONFIG_PRESETS = {
921
1024
  // 24 hours only
922
1025
  showLoadingIndicator: false,
923
1026
  // Silent for security
924
- loadingMessage: "Validating session..."
1027
+ loadingMessage: "Validating session...",
1028
+ refreshOnResume: true
925
1029
  },
926
1030
  advanced: {
927
1031
  responseType: "code",
@@ -955,7 +1059,8 @@ var CONFIG_PRESETS = {
955
1059
  maxRefreshTokenLifetime: 604800,
956
1060
  // 7 days
957
1061
  showLoadingIndicator: false,
958
- loadingMessage: "Refreshing session..."
1062
+ loadingMessage: "Refreshing session...",
1063
+ refreshOnResume: true
959
1064
  },
960
1065
  advanced: {
961
1066
  responseType: "code",
@@ -992,7 +1097,8 @@ var CONFIG_PRESETS = {
992
1097
  // No limit (for convenience)
993
1098
  showLoadingIndicator: true,
994
1099
  // Show for debugging
995
- loadingMessage: "[DEV] Refreshing tokens..."
1100
+ loadingMessage: "[DEV] Refreshing tokens...",
1101
+ refreshOnResume: true
996
1102
  },
997
1103
  advanced: {
998
1104
  responseType: "code",