antaeus.keycloak.react 2.0.0 → 2.1.1

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.js CHANGED
@@ -17,7 +17,7 @@ var defaultConfig = {
17
17
  scope: "openid profile email offline_access",
18
18
  features: {
19
19
  deviceFlow: false,
20
- sessionMonitor: false,
20
+ sessionMonitor: true,
21
21
  autoRefresh: true
22
22
  },
23
23
  silentRenew: {
@@ -62,7 +62,11 @@ var KeycloakConfigBuilder = class {
62
62
  scope: config.scope,
63
63
  automaticSilentRenew: config.silentRenew.enabled,
64
64
  loadUserInfo: config.advanced.loadUserInfo,
65
- silent_redirect_uri: config.silentRenew.silentRedirectUri
65
+ silent_redirect_uri: config.silentRenew.silentRedirectUri,
66
+ // Add PKCE support (required for public clients)
67
+ ...config.advanced.codeChallengeMethod && {
68
+ response_mode: "query"
69
+ }
66
70
  };
67
71
  return oidcSettings;
68
72
  }
@@ -200,14 +204,14 @@ function useTokenLifecycle(options) {
200
204
  log("Refresh already in progress, skipping");
201
205
  return;
202
206
  }
203
- if (checkRefreshTokenLifetime()) {
204
- log("Refresh token exceeded max lifetime, cannot refresh");
205
- setIsRefreshTokenExpired(true);
206
- onRefreshTokenExpired?.();
207
- return;
208
- }
207
+ setIsRefreshing(true);
209
208
  try {
210
- setIsRefreshing(true);
209
+ if (checkRefreshTokenLifetime()) {
210
+ log("Refresh token exceeded max lifetime, cannot refresh");
211
+ setIsRefreshTokenExpired(true);
212
+ onRefreshTokenExpired?.();
213
+ return;
214
+ }
211
215
  onRefreshStart?.();
212
216
  log("Starting token refresh");
213
217
  await auth.signinSilent();
@@ -242,7 +246,11 @@ function useTokenLifecycle(options) {
242
246
  onRefreshTokenExpired?.();
243
247
  const key = getStorageKey();
244
248
  localStorage.removeItem(key);
245
- await auth.removeUser();
249
+ try {
250
+ await auth.removeUser();
251
+ } catch (error) {
252
+ log("Error removing user after max lifetime:", error);
253
+ }
246
254
  return false;
247
255
  }
248
256
  const expiresAt = auth.user.expires_at;
@@ -321,7 +329,9 @@ function useTokenLifecycle(options) {
321
329
  onRefreshTokenExpired?.();
322
330
  const key = getStorageKey();
323
331
  localStorage.removeItem(key);
324
- auth.removeUser();
332
+ auth.removeUser().catch((error) => {
333
+ log("Error removing user during periodic check:", error);
334
+ });
325
335
  }
326
336
  }, 5 * 60 * 1e3);
327
337
  return () => clearInterval(intervalId);
@@ -498,22 +508,28 @@ var SessionMonitor = ({
498
508
  }) => {
499
509
  const auth = useAuth();
500
510
  const [timeRemaining, setTimeRemaining] = react.useState(null);
511
+ const [initialTimeRemaining, setInitialTimeRemaining] = react.useState(null);
501
512
  const [minimized, setMinimized] = react.useState(startMinimized);
502
513
  const [warningTriggered, setWarningTriggered] = react.useState(false);
503
514
  const [criticalTriggered, setCriticalTriggered] = react.useState(false);
504
515
  react.useEffect(() => {
505
516
  if (!auth.isAuthenticated || !auth.user?.expires_at) {
506
517
  setTimeRemaining(null);
518
+ setInitialTimeRemaining(null);
507
519
  return;
508
520
  }
521
+ let logoutTimeoutId = null;
509
522
  const updateTimeRemaining = () => {
510
523
  const expiresAt = auth.user.expires_at;
511
524
  const now = Math.floor(Date.now() / 1e3);
512
525
  const remaining = expiresAt - now;
526
+ if (initialTimeRemaining === null && remaining > 0) {
527
+ setInitialTimeRemaining(remaining);
528
+ }
513
529
  if (remaining <= 0) {
514
530
  setTimeRemaining(0);
515
531
  onExpired?.();
516
- setTimeout(() => {
532
+ logoutTimeoutId = setTimeout(() => {
517
533
  auth.signoutRedirect();
518
534
  }, 2e3);
519
535
  } else {
@@ -529,13 +545,19 @@ var SessionMonitor = ({
529
545
  };
530
546
  updateTimeRemaining();
531
547
  const interval = setInterval(updateTimeRemaining, 1e3);
532
- return () => clearInterval(interval);
548
+ return () => {
549
+ clearInterval(interval);
550
+ if (logoutTimeoutId) {
551
+ clearTimeout(logoutTimeoutId);
552
+ }
553
+ };
533
554
  }, [
534
555
  auth,
535
556
  warningThreshold,
536
557
  criticalThreshold,
537
558
  warningTriggered,
538
559
  criticalTriggered,
560
+ initialTimeRemaining,
539
561
  onWarning,
540
562
  onCritical,
541
563
  onExpired
@@ -558,9 +580,9 @@ var SessionMonitor = ({
558
580
  return "normal";
559
581
  }, [timeRemaining, warningThreshold, criticalThreshold]);
560
582
  const getProgress = react.useCallback(() => {
561
- if (!timeRemaining || !auth.user?.expires_in) return 100;
562
- return timeRemaining / auth.user.expires_in * 100;
563
- }, [timeRemaining, auth.user]);
583
+ if (!timeRemaining || !initialTimeRemaining) return 100;
584
+ return Math.max(0, Math.min(100, timeRemaining / initialTimeRemaining * 100));
585
+ }, [timeRemaining, initialTimeRemaining]);
564
586
  if (!auth.isAuthenticated || timeRemaining === null) {
565
587
  return null;
566
588
  }
@@ -873,12 +895,27 @@ function buildZeroConfig(props) {
873
895
  const { authority, clientId, preset = "default", config: overrides = {} } = props;
874
896
  const presetConfig = mergeWithPreset(preset, overrides);
875
897
  const fullConfig = {
876
- ...defaultConfig,
877
- ...presetConfig,
878
898
  authority,
879
899
  clientId,
880
- redirectUri: presetConfig.redirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/"),
881
- postLogoutRedirectUri: presetConfig.postLogoutRedirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/")
900
+ redirectUri: presetConfig.redirectUri || defaultConfig.redirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/"),
901
+ postLogoutRedirectUri: presetConfig.postLogoutRedirectUri || defaultConfig.postLogoutRedirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/"),
902
+ scope: presetConfig.scope || defaultConfig.scope,
903
+ features: {
904
+ ...defaultConfig.features,
905
+ ...presetConfig.features
906
+ },
907
+ silentRenew: {
908
+ ...defaultConfig.silentRenew,
909
+ ...presetConfig.silentRenew
910
+ },
911
+ advanced: {
912
+ ...defaultConfig.advanced,
913
+ ...presetConfig.advanced
914
+ },
915
+ events: {
916
+ ...defaultConfig.events,
917
+ ...presetConfig.events
918
+ }
882
919
  };
883
920
  return fullConfig;
884
921
  }
@@ -973,6 +1010,7 @@ var ProtectedRoute = ({
973
1010
  const { hasAnyRole, hasAllRoles } = useRoles();
974
1011
  const { hasAnyScope, hasAllScopes } = useScopes();
975
1012
  const location = reactRouterDom.useLocation();
1013
+ const hasRedirected = react.useRef(false);
976
1014
  const isAuthenticated = !auth.isLoading && auth.isAuthenticated;
977
1015
  const policyAccess = policy && auth.user ? policy(auth.user, { location }) : null;
978
1016
  const roleAccess = requiredRoles.length > 0 ? requireAllRoles ? hasAllRoles(requiredRoles) : hasAnyRole(requiredRoles) : null;
@@ -984,12 +1022,21 @@ var ProtectedRoute = ({
984
1022
  }
985
1023
  }, [unauthorizedReason, onUnauthorized]);
986
1024
  react.useEffect(() => {
987
- if (!auth.isLoading && !auth.isAuthenticated) {
1025
+ if (!auth.isLoading && !auth.isAuthenticated && !hasRedirected.current) {
1026
+ hasRedirected.current = true;
988
1027
  auth.signinRedirect({
989
1028
  redirect_uri: window.location.origin + location.pathname + location.search
1029
+ }).catch((error) => {
1030
+ hasRedirected.current = false;
1031
+ console.error("[ProtectedRoute] Signin redirect failed:", error);
990
1032
  });
991
1033
  }
992
- }, [auth.isLoading, auth.isAuthenticated, auth, location.pathname, location.search]);
1034
+ }, [auth.isLoading, auth.isAuthenticated, location.pathname, location.search]);
1035
+ react.useEffect(() => {
1036
+ if (auth.isAuthenticated) {
1037
+ hasRedirected.current = false;
1038
+ }
1039
+ }, [auth.isAuthenticated]);
993
1040
  if (auth.isLoading) {
994
1041
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent || /* @__PURE__ */ jsxRuntime.jsx(DefaultLoadingComponent, {}) });
995
1042
  }
@@ -1357,8 +1404,17 @@ var DeviceLogin = ({
1357
1404
  });
1358
1405
  if (userManagerRef.current) {
1359
1406
  await userManagerRef.current.storeUser(user);
1407
+ const events = userManagerRef.current.events;
1408
+ if (events && events._userLoaded) {
1409
+ events._userLoaded.raise(user);
1410
+ }
1411
+ }
1412
+ if (onSuccess) {
1413
+ await new Promise((resolve) => setTimeout(resolve, 100));
1414
+ onSuccess(user);
1415
+ } else {
1416
+ window.location.reload();
1360
1417
  }
1361
- onSuccess?.(user);
1362
1418
  }
1363
1419
  } catch (err) {
1364
1420
  if (!err.message.includes("authorization_pending")) {
@@ -1445,14 +1501,14 @@ function useProtectedFetch(options = {}) {
1445
1501
  },
1446
1502
  [baseUrl]
1447
1503
  );
1448
- const getAuthHeader = react.useCallback(() => {
1504
+ const getAuthHeader = () => {
1449
1505
  if (!auth.user?.access_token) {
1450
1506
  return {};
1451
1507
  }
1452
1508
  return {
1453
1509
  Authorization: `Bearer ${auth.user.access_token}`
1454
1510
  };
1455
- }, [auth.user]);
1511
+ };
1456
1512
  const buildHeaders = react.useCallback(
1457
1513
  (requestHeaders) => {
1458
1514
  const headers = new Headers(requestHeaders);
@@ -1465,7 +1521,7 @@ function useProtectedFetch(options = {}) {
1465
1521
  });
1466
1522
  return headers;
1467
1523
  },
1468
- [getAuthHeader, customHeaders]
1524
+ [customHeaders]
1469
1525
  );
1470
1526
  const protectedFetch = react.useCallback(
1471
1527
  async (url, init) => {