antaeus.keycloak.react 2.3.0 → 2.5.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 +77 -0
- package/dist/index.d.mts +108 -23
- package/dist/index.d.ts +108 -23
- package/dist/index.js +253 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +250 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createContext, useState, useRef, useCallback, useEffect, useMemo
|
|
2
|
-
import { useAuth as useAuth$1, AuthProvider } from 'react-oidc-context';
|
|
1
|
+
import { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useAuth as useAuth$1, AuthContext, AuthProvider } from 'react-oidc-context';
|
|
3
3
|
import { WebStorageStateStore, User } from 'oidc-client-ts';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
import { useLocation } from 'react-router-dom';
|
|
@@ -149,6 +149,14 @@ var useAuth = useAuth$1;
|
|
|
149
149
|
|
|
150
150
|
// src/hooks/useTokenLifecycle.ts
|
|
151
151
|
var STORAGE_KEY_PREFIX = "keycloak_first_login_";
|
|
152
|
+
var isNavigatorOffline = () => typeof navigator !== "undefined" && "onLine" in navigator && navigator.onLine === false;
|
|
153
|
+
var isTransientNetworkError = (error) => {
|
|
154
|
+
const message = error.message?.toLowerCase?.() ?? "";
|
|
155
|
+
if (!message) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
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");
|
|
159
|
+
};
|
|
152
160
|
function useTokenLifecycle(options) {
|
|
153
161
|
const {
|
|
154
162
|
config,
|
|
@@ -163,6 +171,11 @@ function useTokenLifecycle(options) {
|
|
|
163
171
|
const [isRefreshTokenExpired, setIsRefreshTokenExpired] = useState(false);
|
|
164
172
|
const hasCheckedOnStartup = useRef(false);
|
|
165
173
|
const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
|
|
174
|
+
const [pendingRetry, setPendingRetry] = useState(null);
|
|
175
|
+
const deferredRefreshReasonRef = useRef(null);
|
|
176
|
+
const enqueueRetry = useCallback((reason) => {
|
|
177
|
+
setPendingRetry((current) => current === reason ? current : reason);
|
|
178
|
+
}, []);
|
|
166
179
|
const log = useCallback(
|
|
167
180
|
(message, ...args) => {
|
|
168
181
|
if (debug) {
|
|
@@ -213,6 +226,7 @@ function useTokenLifecycle(options) {
|
|
|
213
226
|
return;
|
|
214
227
|
}
|
|
215
228
|
setIsRefreshing(true);
|
|
229
|
+
deferredRefreshReasonRef.current = null;
|
|
216
230
|
try {
|
|
217
231
|
if (checkRefreshTokenLifetime()) {
|
|
218
232
|
log("Refresh token exceeded max lifetime, cannot refresh");
|
|
@@ -220,6 +234,12 @@ function useTokenLifecycle(options) {
|
|
|
220
234
|
onRefreshTokenExpired?.();
|
|
221
235
|
return;
|
|
222
236
|
}
|
|
237
|
+
if (isNavigatorOffline()) {
|
|
238
|
+
log("Device offline, deferring token refresh until connection is restored");
|
|
239
|
+
deferredRefreshReasonRef.current = "offline";
|
|
240
|
+
enqueueRetry("offline");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
223
243
|
onRefreshStart?.();
|
|
224
244
|
log("Starting token refresh");
|
|
225
245
|
await auth.signinSilent();
|
|
@@ -227,6 +247,21 @@ function useTokenLifecycle(options) {
|
|
|
227
247
|
onRefreshSuccess?.();
|
|
228
248
|
} catch (error) {
|
|
229
249
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
250
|
+
if (isTransientNetworkError(err)) {
|
|
251
|
+
const reason = isNavigatorOffline() ? "offline" : "network";
|
|
252
|
+
deferredRefreshReasonRef.current = reason;
|
|
253
|
+
if (reason === "offline") {
|
|
254
|
+
log("Token refresh deferred because device is offline");
|
|
255
|
+
} else {
|
|
256
|
+
log(
|
|
257
|
+
"Token refresh failed due to transient network issue, will retry automatically:",
|
|
258
|
+
err.message
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
enqueueRetry(reason);
|
|
262
|
+
onRefreshError?.(err);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
230
265
|
log("Token refresh failed:", err.message);
|
|
231
266
|
onRefreshError?.(err);
|
|
232
267
|
throw err;
|
|
@@ -241,6 +276,7 @@ function useTokenLifecycle(options) {
|
|
|
241
276
|
onRefreshSuccess,
|
|
242
277
|
onRefreshError,
|
|
243
278
|
onRefreshTokenExpired,
|
|
279
|
+
enqueueRetry,
|
|
244
280
|
log
|
|
245
281
|
]);
|
|
246
282
|
const checkAndRefreshTokens = useCallback(async () => {
|
|
@@ -279,6 +315,14 @@ function useTokenLifecycle(options) {
|
|
|
279
315
|
if (timeUntilExpiry <= threshold) {
|
|
280
316
|
try {
|
|
281
317
|
await refreshToken();
|
|
318
|
+
if (deferredRefreshReasonRef.current) {
|
|
319
|
+
log(
|
|
320
|
+
"Token refresh was deferred due to pending retry:",
|
|
321
|
+
deferredRefreshReasonRef.current
|
|
322
|
+
);
|
|
323
|
+
deferredRefreshReasonRef.current = null;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
282
326
|
return true;
|
|
283
327
|
} catch (error) {
|
|
284
328
|
log("Failed to refresh tokens:", error);
|
|
@@ -296,6 +340,55 @@ function useTokenLifecycle(options) {
|
|
|
296
340
|
getStorageKey,
|
|
297
341
|
log
|
|
298
342
|
]);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (pendingRetry === null) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (typeof window === "undefined") {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
let cancelled = false;
|
|
351
|
+
const executeRetry = () => {
|
|
352
|
+
if (cancelled) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (isRefreshing) {
|
|
356
|
+
log("Skipping scheduled token refresh retry because a refresh is already in progress");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
refreshToken().catch((err) => {
|
|
360
|
+
log("Scheduled token refresh retry failed:", err);
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
if (pendingRetry === "offline") {
|
|
364
|
+
log("Waiting for online event to retry token refresh");
|
|
365
|
+
const handleOnline = () => {
|
|
366
|
+
if (cancelled) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
log("Online event detected, retrying deferred token refresh");
|
|
370
|
+
setPendingRetry(null);
|
|
371
|
+
executeRetry();
|
|
372
|
+
};
|
|
373
|
+
window.addEventListener("online", handleOnline, { once: true });
|
|
374
|
+
return () => {
|
|
375
|
+
cancelled = true;
|
|
376
|
+
window.removeEventListener("online", handleOnline);
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const timeoutId = window.setTimeout(() => {
|
|
380
|
+
if (cancelled) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
log("Retrying token refresh after transient network failure delay");
|
|
384
|
+
setPendingRetry(null);
|
|
385
|
+
executeRetry();
|
|
386
|
+
}, 5e3);
|
|
387
|
+
return () => {
|
|
388
|
+
cancelled = true;
|
|
389
|
+
window.clearTimeout(timeoutId);
|
|
390
|
+
};
|
|
391
|
+
}, [pendingRetry, refreshToken, isRefreshing, log]);
|
|
299
392
|
useEffect(() => {
|
|
300
393
|
const strategy = config.silentRenew?.refreshStrategy || "both";
|
|
301
394
|
const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
|
|
@@ -656,6 +749,71 @@ var SessionMonitor = ({
|
|
|
656
749
|
] });
|
|
657
750
|
};
|
|
658
751
|
|
|
752
|
+
// src/utils/tokenBridge.ts
|
|
753
|
+
var TokenBridge = class {
|
|
754
|
+
constructor() {
|
|
755
|
+
this.currentToken = null;
|
|
756
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Update the tracked token and notify subscribers when it changes.
|
|
760
|
+
*/
|
|
761
|
+
setToken(token) {
|
|
762
|
+
if (this.currentToken === token) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
this.currentToken = token;
|
|
766
|
+
for (const listener of this.listeners) {
|
|
767
|
+
try {
|
|
768
|
+
listener(token);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
console.error("[TokenBridge] Listener threw error:", error);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Read the most recent token value.
|
|
776
|
+
*/
|
|
777
|
+
getToken() {
|
|
778
|
+
return this.currentToken;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Convenience helper for async callers.
|
|
782
|
+
*/
|
|
783
|
+
async getTokenAsync() {
|
|
784
|
+
return this.currentToken;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Subscribe to token changes. Returns an unsubscribe callback.
|
|
788
|
+
*/
|
|
789
|
+
subscribe(listener) {
|
|
790
|
+
this.listeners.add(listener);
|
|
791
|
+
listener(this.currentToken);
|
|
792
|
+
return () => {
|
|
793
|
+
this.listeners.delete(listener);
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
var createTokenBridge = () => new TokenBridge();
|
|
798
|
+
var defaultTokenBridge = createTokenBridge();
|
|
799
|
+
|
|
800
|
+
// src/components/TokenBridge.tsx
|
|
801
|
+
var TokenBridgeSync = ({
|
|
802
|
+
bridge = defaultTokenBridge
|
|
803
|
+
}) => {
|
|
804
|
+
const auth = useAuth();
|
|
805
|
+
useEffect(() => {
|
|
806
|
+
const token = auth.user?.access_token ?? null;
|
|
807
|
+
bridge.setToken(token);
|
|
808
|
+
}, [auth.user, bridge]);
|
|
809
|
+
useEffect(() => {
|
|
810
|
+
if (!auth.isAuthenticated) {
|
|
811
|
+
bridge.setToken(null);
|
|
812
|
+
}
|
|
813
|
+
}, [auth.isAuthenticated, bridge]);
|
|
814
|
+
return null;
|
|
815
|
+
};
|
|
816
|
+
|
|
659
817
|
// src/config/presets.ts
|
|
660
818
|
var CONFIG_PRESETS = {
|
|
661
819
|
/**
|
|
@@ -874,11 +1032,97 @@ var StorageConfigProvider = ({ storageType, children }) => {
|
|
|
874
1032
|
return /* @__PURE__ */ jsx(StorageConfigContext.Provider, { value: { storageType, storage }, children });
|
|
875
1033
|
};
|
|
876
1034
|
var useStorageConfig = () => useContext(StorageConfigContext);
|
|
1035
|
+
var defaultStatus = {
|
|
1036
|
+
isEnabled: true,
|
|
1037
|
+
isConfigured: true
|
|
1038
|
+
};
|
|
1039
|
+
var KeycloakStatusContext = createContext(defaultStatus);
|
|
1040
|
+
var KeycloakStatusProvider = KeycloakStatusContext.Provider;
|
|
1041
|
+
var useKeycloakStatus = () => useContext(KeycloakStatusContext);
|
|
1042
|
+
var createDisabledEvents = () => {
|
|
1043
|
+
const noop = () => void 0;
|
|
1044
|
+
const noopSubscription = () => () => void 0;
|
|
1045
|
+
const noopAsync = async () => void 0;
|
|
1046
|
+
const events = {
|
|
1047
|
+
addAccessTokenExpiring: noopSubscription,
|
|
1048
|
+
removeAccessTokenExpiring: noop,
|
|
1049
|
+
addAccessTokenExpired: noopSubscription,
|
|
1050
|
+
removeAccessTokenExpired: noop,
|
|
1051
|
+
addSilentRenewError: noopSubscription,
|
|
1052
|
+
removeSilentRenewError: noop,
|
|
1053
|
+
addUserLoaded: noopSubscription,
|
|
1054
|
+
removeUserLoaded: noop,
|
|
1055
|
+
addUserUnloaded: noopSubscription,
|
|
1056
|
+
removeUserUnloaded: noop,
|
|
1057
|
+
addUserSignedIn: noopSubscription,
|
|
1058
|
+
removeUserSignedIn: noop,
|
|
1059
|
+
addUserSignedOut: noopSubscription,
|
|
1060
|
+
removeUserSignedOut: noop,
|
|
1061
|
+
addUserSessionChanged: noopSubscription,
|
|
1062
|
+
removeUserSessionChanged: noop,
|
|
1063
|
+
load: noopAsync,
|
|
1064
|
+
unload: noopAsync
|
|
1065
|
+
};
|
|
1066
|
+
return events;
|
|
1067
|
+
};
|
|
1068
|
+
var disabledEvents = createDisabledEvents();
|
|
1069
|
+
var disabledSettings = {
|
|
1070
|
+
authority: "",
|
|
1071
|
+
client_id: "",
|
|
1072
|
+
redirect_uri: "http://localhost/disabled",
|
|
1073
|
+
response_type: "code",
|
|
1074
|
+
scope: ""
|
|
1075
|
+
};
|
|
1076
|
+
var disabledError = () => new Error("Keycloak integration is disabled for this environment.");
|
|
1077
|
+
var disabledAsync = async () => {
|
|
1078
|
+
throw disabledError();
|
|
1079
|
+
};
|
|
1080
|
+
var disabledAuthContext = {
|
|
1081
|
+
settings: disabledSettings,
|
|
1082
|
+
events: disabledEvents,
|
|
1083
|
+
clearStaleState: async () => void 0,
|
|
1084
|
+
removeUser: async () => void 0,
|
|
1085
|
+
signinPopup: (_args) => disabledAsync(),
|
|
1086
|
+
signinSilent: async (_args) => null,
|
|
1087
|
+
signinRedirect: (_args) => disabledAsync(),
|
|
1088
|
+
signinResourceOwnerCredentials: (_args) => disabledAsync(),
|
|
1089
|
+
signoutRedirect: async (_args) => void 0,
|
|
1090
|
+
signoutPopup: async (_args) => void 0,
|
|
1091
|
+
signoutSilent: async (_args) => void 0,
|
|
1092
|
+
querySessionStatus: async (_args) => null,
|
|
1093
|
+
revokeTokens: async (_types) => void 0,
|
|
1094
|
+
startSilentRenew: () => void 0,
|
|
1095
|
+
stopSilentRenew: () => void 0,
|
|
1096
|
+
user: null,
|
|
1097
|
+
isLoading: false,
|
|
1098
|
+
isAuthenticated: false,
|
|
1099
|
+
activeNavigator: void 0,
|
|
1100
|
+
error: void 0
|
|
1101
|
+
};
|
|
877
1102
|
function isZeroConfig(props) {
|
|
878
1103
|
return "authority" in props && "clientId" in props;
|
|
879
1104
|
}
|
|
880
1105
|
var KeycloakProvider = (props) => {
|
|
881
1106
|
const { children, debug = false } = props;
|
|
1107
|
+
const status = useMemo(() => {
|
|
1108
|
+
const enabledFlag = props.enabled ?? true;
|
|
1109
|
+
const hasAuthority = isZeroConfig(props) ? Boolean(props.authority) : Boolean(props.config.authority);
|
|
1110
|
+
const hasClientId = isZeroConfig(props) ? Boolean(props.clientId) : Boolean(props.config.clientId);
|
|
1111
|
+
return {
|
|
1112
|
+
isEnabled: enabledFlag,
|
|
1113
|
+
isConfigured: hasAuthority && hasClientId,
|
|
1114
|
+
reason: !enabledFlag ? "disabled" : !hasAuthority || !hasClientId ? "missing-config" : void 0
|
|
1115
|
+
};
|
|
1116
|
+
}, [props]);
|
|
1117
|
+
const effectiveEnabled = status.isEnabled && status.isConfigured;
|
|
1118
|
+
useEffect(() => {
|
|
1119
|
+
if (!effectiveEnabled && props.tokenBridge) {
|
|
1120
|
+
props.tokenBridge.setToken(null);
|
|
1121
|
+
}
|
|
1122
|
+
}, [effectiveEnabled, props.tokenBridge]);
|
|
1123
|
+
if (!effectiveEnabled) {
|
|
1124
|
+
return /* @__PURE__ */ jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsx(AuthContext.Provider, { value: disabledAuthContext, children: /* @__PURE__ */ jsx(StorageConfigProvider, { storageType: "session", children }) }) });
|
|
1125
|
+
}
|
|
882
1126
|
const config = isZeroConfig(props) ? buildZeroConfig(props) : props.config;
|
|
883
1127
|
const oidcConfig = KeycloakConfigBuilder.build(config);
|
|
884
1128
|
const log = useCallback(
|
|
@@ -920,7 +1164,7 @@ var KeycloakProvider = (props) => {
|
|
|
920
1164
|
}, [log]);
|
|
921
1165
|
const showSessionMonitor = config.features?.sessionMonitor !== false;
|
|
922
1166
|
const storageType = config.advanced?.storageType || "session";
|
|
923
|
-
return /* @__PURE__ */ jsx(
|
|
1167
|
+
return /* @__PURE__ */ jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsx(
|
|
924
1168
|
AuthProvider,
|
|
925
1169
|
{
|
|
926
1170
|
...oidcConfig,
|
|
@@ -928,11 +1172,12 @@ var KeycloakProvider = (props) => {
|
|
|
928
1172
|
onSignoutCallback,
|
|
929
1173
|
onRemoveUser,
|
|
930
1174
|
children: /* @__PURE__ */ jsx(StorageConfigProvider, { storageType, children: /* @__PURE__ */ jsxs(TokenLifecycleManager, { config, debug, children: [
|
|
1175
|
+
props.tokenBridge && /* @__PURE__ */ jsx(TokenBridgeSync, { bridge: props.tokenBridge }),
|
|
931
1176
|
showSessionMonitor && /* @__PURE__ */ jsx(SessionMonitor, {}),
|
|
932
1177
|
children
|
|
933
1178
|
] }) })
|
|
934
1179
|
}
|
|
935
|
-
);
|
|
1180
|
+
) });
|
|
936
1181
|
};
|
|
937
1182
|
function buildZeroConfig(props) {
|
|
938
1183
|
const { authority, clientId, preset = "default", config: overrides = {} } = props;
|
|
@@ -1636,6 +1881,6 @@ function useProtectedFetch(options = {}) {
|
|
|
1636
1881
|
};
|
|
1637
1882
|
}
|
|
1638
1883
|
|
|
1639
|
-
export { CONFIG_PRESETS, DeviceLogin, KeycloakConfigBuilder, KeycloakProvider, LoginButton, LogoutButton, ProtectedRoute, SessionMonitor, TokenLifecycleManager, UserProfile, defaultConfig, fetchUserInfo, getConfigFromEnv, getConfigPreset, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useProtectedFetch, useRoles, useScopes, useTokenLifecycle };
|
|
1884
|
+
export { CONFIG_PRESETS, DeviceLogin, KeycloakConfigBuilder, KeycloakProvider, KeycloakStatusProvider, LoginButton, LogoutButton, ProtectedRoute, SessionMonitor, TokenBridge, TokenBridgeSync, TokenLifecycleManager, UserProfile, createTokenBridge, defaultConfig, defaultTokenBridge, fetchUserInfo, getConfigFromEnv, getConfigPreset, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useKeycloakStatus, useProtectedFetch, useRoles, useScopes, useTokenLifecycle };
|
|
1640
1885
|
//# sourceMappingURL=index.mjs.map
|
|
1641
1886
|
//# sourceMappingURL=index.mjs.map
|