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.js
CHANGED
|
@@ -152,6 +152,14 @@ var useAuth = reactOidcContext.useAuth;
|
|
|
152
152
|
|
|
153
153
|
// src/hooks/useTokenLifecycle.ts
|
|
154
154
|
var STORAGE_KEY_PREFIX = "keycloak_first_login_";
|
|
155
|
+
var isNavigatorOffline = () => typeof navigator !== "undefined" && "onLine" in navigator && navigator.onLine === false;
|
|
156
|
+
var isTransientNetworkError = (error) => {
|
|
157
|
+
const message = error.message?.toLowerCase?.() ?? "";
|
|
158
|
+
if (!message) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
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");
|
|
162
|
+
};
|
|
155
163
|
function useTokenLifecycle(options) {
|
|
156
164
|
const {
|
|
157
165
|
config,
|
|
@@ -166,6 +174,11 @@ function useTokenLifecycle(options) {
|
|
|
166
174
|
const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
|
|
167
175
|
const hasCheckedOnStartup = react.useRef(false);
|
|
168
176
|
const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
|
|
177
|
+
const [pendingRetry, setPendingRetry] = react.useState(null);
|
|
178
|
+
const deferredRefreshReasonRef = react.useRef(null);
|
|
179
|
+
const enqueueRetry = react.useCallback((reason) => {
|
|
180
|
+
setPendingRetry((current) => current === reason ? current : reason);
|
|
181
|
+
}, []);
|
|
169
182
|
const log = react.useCallback(
|
|
170
183
|
(message, ...args) => {
|
|
171
184
|
if (debug) {
|
|
@@ -216,6 +229,7 @@ function useTokenLifecycle(options) {
|
|
|
216
229
|
return;
|
|
217
230
|
}
|
|
218
231
|
setIsRefreshing(true);
|
|
232
|
+
deferredRefreshReasonRef.current = null;
|
|
219
233
|
try {
|
|
220
234
|
if (checkRefreshTokenLifetime()) {
|
|
221
235
|
log("Refresh token exceeded max lifetime, cannot refresh");
|
|
@@ -223,6 +237,12 @@ function useTokenLifecycle(options) {
|
|
|
223
237
|
onRefreshTokenExpired?.();
|
|
224
238
|
return;
|
|
225
239
|
}
|
|
240
|
+
if (isNavigatorOffline()) {
|
|
241
|
+
log("Device offline, deferring token refresh until connection is restored");
|
|
242
|
+
deferredRefreshReasonRef.current = "offline";
|
|
243
|
+
enqueueRetry("offline");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
226
246
|
onRefreshStart?.();
|
|
227
247
|
log("Starting token refresh");
|
|
228
248
|
await auth.signinSilent();
|
|
@@ -230,6 +250,21 @@ function useTokenLifecycle(options) {
|
|
|
230
250
|
onRefreshSuccess?.();
|
|
231
251
|
} catch (error) {
|
|
232
252
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
253
|
+
if (isTransientNetworkError(err)) {
|
|
254
|
+
const reason = isNavigatorOffline() ? "offline" : "network";
|
|
255
|
+
deferredRefreshReasonRef.current = reason;
|
|
256
|
+
if (reason === "offline") {
|
|
257
|
+
log("Token refresh deferred because device is offline");
|
|
258
|
+
} else {
|
|
259
|
+
log(
|
|
260
|
+
"Token refresh failed due to transient network issue, will retry automatically:",
|
|
261
|
+
err.message
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
enqueueRetry(reason);
|
|
265
|
+
onRefreshError?.(err);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
233
268
|
log("Token refresh failed:", err.message);
|
|
234
269
|
onRefreshError?.(err);
|
|
235
270
|
throw err;
|
|
@@ -244,6 +279,7 @@ function useTokenLifecycle(options) {
|
|
|
244
279
|
onRefreshSuccess,
|
|
245
280
|
onRefreshError,
|
|
246
281
|
onRefreshTokenExpired,
|
|
282
|
+
enqueueRetry,
|
|
247
283
|
log
|
|
248
284
|
]);
|
|
249
285
|
const checkAndRefreshTokens = react.useCallback(async () => {
|
|
@@ -282,6 +318,14 @@ function useTokenLifecycle(options) {
|
|
|
282
318
|
if (timeUntilExpiry <= threshold) {
|
|
283
319
|
try {
|
|
284
320
|
await refreshToken();
|
|
321
|
+
if (deferredRefreshReasonRef.current) {
|
|
322
|
+
log(
|
|
323
|
+
"Token refresh was deferred due to pending retry:",
|
|
324
|
+
deferredRefreshReasonRef.current
|
|
325
|
+
);
|
|
326
|
+
deferredRefreshReasonRef.current = null;
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
285
329
|
return true;
|
|
286
330
|
} catch (error) {
|
|
287
331
|
log("Failed to refresh tokens:", error);
|
|
@@ -299,6 +343,55 @@ function useTokenLifecycle(options) {
|
|
|
299
343
|
getStorageKey,
|
|
300
344
|
log
|
|
301
345
|
]);
|
|
346
|
+
react.useEffect(() => {
|
|
347
|
+
if (pendingRetry === null) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (typeof window === "undefined") {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
let cancelled = false;
|
|
354
|
+
const executeRetry = () => {
|
|
355
|
+
if (cancelled) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (isRefreshing) {
|
|
359
|
+
log("Skipping scheduled token refresh retry because a refresh is already in progress");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
refreshToken().catch((err) => {
|
|
363
|
+
log("Scheduled token refresh retry failed:", err);
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
if (pendingRetry === "offline") {
|
|
367
|
+
log("Waiting for online event to retry token refresh");
|
|
368
|
+
const handleOnline = () => {
|
|
369
|
+
if (cancelled) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
log("Online event detected, retrying deferred token refresh");
|
|
373
|
+
setPendingRetry(null);
|
|
374
|
+
executeRetry();
|
|
375
|
+
};
|
|
376
|
+
window.addEventListener("online", handleOnline, { once: true });
|
|
377
|
+
return () => {
|
|
378
|
+
cancelled = true;
|
|
379
|
+
window.removeEventListener("online", handleOnline);
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const timeoutId = window.setTimeout(() => {
|
|
383
|
+
if (cancelled) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
log("Retrying token refresh after transient network failure delay");
|
|
387
|
+
setPendingRetry(null);
|
|
388
|
+
executeRetry();
|
|
389
|
+
}, 5e3);
|
|
390
|
+
return () => {
|
|
391
|
+
cancelled = true;
|
|
392
|
+
window.clearTimeout(timeoutId);
|
|
393
|
+
};
|
|
394
|
+
}, [pendingRetry, refreshToken, isRefreshing, log]);
|
|
302
395
|
react.useEffect(() => {
|
|
303
396
|
const strategy = config.silentRenew?.refreshStrategy || "both";
|
|
304
397
|
const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
|
|
@@ -659,6 +752,71 @@ var SessionMonitor = ({
|
|
|
659
752
|
] });
|
|
660
753
|
};
|
|
661
754
|
|
|
755
|
+
// src/utils/tokenBridge.ts
|
|
756
|
+
var TokenBridge = class {
|
|
757
|
+
constructor() {
|
|
758
|
+
this.currentToken = null;
|
|
759
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Update the tracked token and notify subscribers when it changes.
|
|
763
|
+
*/
|
|
764
|
+
setToken(token) {
|
|
765
|
+
if (this.currentToken === token) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
this.currentToken = token;
|
|
769
|
+
for (const listener of this.listeners) {
|
|
770
|
+
try {
|
|
771
|
+
listener(token);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.error("[TokenBridge] Listener threw error:", error);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Read the most recent token value.
|
|
779
|
+
*/
|
|
780
|
+
getToken() {
|
|
781
|
+
return this.currentToken;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Convenience helper for async callers.
|
|
785
|
+
*/
|
|
786
|
+
async getTokenAsync() {
|
|
787
|
+
return this.currentToken;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Subscribe to token changes. Returns an unsubscribe callback.
|
|
791
|
+
*/
|
|
792
|
+
subscribe(listener) {
|
|
793
|
+
this.listeners.add(listener);
|
|
794
|
+
listener(this.currentToken);
|
|
795
|
+
return () => {
|
|
796
|
+
this.listeners.delete(listener);
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
var createTokenBridge = () => new TokenBridge();
|
|
801
|
+
var defaultTokenBridge = createTokenBridge();
|
|
802
|
+
|
|
803
|
+
// src/components/TokenBridge.tsx
|
|
804
|
+
var TokenBridgeSync = ({
|
|
805
|
+
bridge = defaultTokenBridge
|
|
806
|
+
}) => {
|
|
807
|
+
const auth = useAuth();
|
|
808
|
+
react.useEffect(() => {
|
|
809
|
+
const token = auth.user?.access_token ?? null;
|
|
810
|
+
bridge.setToken(token);
|
|
811
|
+
}, [auth.user, bridge]);
|
|
812
|
+
react.useEffect(() => {
|
|
813
|
+
if (!auth.isAuthenticated) {
|
|
814
|
+
bridge.setToken(null);
|
|
815
|
+
}
|
|
816
|
+
}, [auth.isAuthenticated, bridge]);
|
|
817
|
+
return null;
|
|
818
|
+
};
|
|
819
|
+
|
|
662
820
|
// src/config/presets.ts
|
|
663
821
|
var CONFIG_PRESETS = {
|
|
664
822
|
/**
|
|
@@ -877,11 +1035,97 @@ var StorageConfigProvider = ({ storageType, children }) => {
|
|
|
877
1035
|
return /* @__PURE__ */ jsxRuntime.jsx(StorageConfigContext.Provider, { value: { storageType, storage }, children });
|
|
878
1036
|
};
|
|
879
1037
|
var useStorageConfig = () => react.useContext(StorageConfigContext);
|
|
1038
|
+
var defaultStatus = {
|
|
1039
|
+
isEnabled: true,
|
|
1040
|
+
isConfigured: true
|
|
1041
|
+
};
|
|
1042
|
+
var KeycloakStatusContext = react.createContext(defaultStatus);
|
|
1043
|
+
var KeycloakStatusProvider = KeycloakStatusContext.Provider;
|
|
1044
|
+
var useKeycloakStatus = () => react.useContext(KeycloakStatusContext);
|
|
1045
|
+
var createDisabledEvents = () => {
|
|
1046
|
+
const noop = () => void 0;
|
|
1047
|
+
const noopSubscription = () => () => void 0;
|
|
1048
|
+
const noopAsync = async () => void 0;
|
|
1049
|
+
const events = {
|
|
1050
|
+
addAccessTokenExpiring: noopSubscription,
|
|
1051
|
+
removeAccessTokenExpiring: noop,
|
|
1052
|
+
addAccessTokenExpired: noopSubscription,
|
|
1053
|
+
removeAccessTokenExpired: noop,
|
|
1054
|
+
addSilentRenewError: noopSubscription,
|
|
1055
|
+
removeSilentRenewError: noop,
|
|
1056
|
+
addUserLoaded: noopSubscription,
|
|
1057
|
+
removeUserLoaded: noop,
|
|
1058
|
+
addUserUnloaded: noopSubscription,
|
|
1059
|
+
removeUserUnloaded: noop,
|
|
1060
|
+
addUserSignedIn: noopSubscription,
|
|
1061
|
+
removeUserSignedIn: noop,
|
|
1062
|
+
addUserSignedOut: noopSubscription,
|
|
1063
|
+
removeUserSignedOut: noop,
|
|
1064
|
+
addUserSessionChanged: noopSubscription,
|
|
1065
|
+
removeUserSessionChanged: noop,
|
|
1066
|
+
load: noopAsync,
|
|
1067
|
+
unload: noopAsync
|
|
1068
|
+
};
|
|
1069
|
+
return events;
|
|
1070
|
+
};
|
|
1071
|
+
var disabledEvents = createDisabledEvents();
|
|
1072
|
+
var disabledSettings = {
|
|
1073
|
+
authority: "",
|
|
1074
|
+
client_id: "",
|
|
1075
|
+
redirect_uri: "http://localhost/disabled",
|
|
1076
|
+
response_type: "code",
|
|
1077
|
+
scope: ""
|
|
1078
|
+
};
|
|
1079
|
+
var disabledError = () => new Error("Keycloak integration is disabled for this environment.");
|
|
1080
|
+
var disabledAsync = async () => {
|
|
1081
|
+
throw disabledError();
|
|
1082
|
+
};
|
|
1083
|
+
var disabledAuthContext = {
|
|
1084
|
+
settings: disabledSettings,
|
|
1085
|
+
events: disabledEvents,
|
|
1086
|
+
clearStaleState: async () => void 0,
|
|
1087
|
+
removeUser: async () => void 0,
|
|
1088
|
+
signinPopup: (_args) => disabledAsync(),
|
|
1089
|
+
signinSilent: async (_args) => null,
|
|
1090
|
+
signinRedirect: (_args) => disabledAsync(),
|
|
1091
|
+
signinResourceOwnerCredentials: (_args) => disabledAsync(),
|
|
1092
|
+
signoutRedirect: async (_args) => void 0,
|
|
1093
|
+
signoutPopup: async (_args) => void 0,
|
|
1094
|
+
signoutSilent: async (_args) => void 0,
|
|
1095
|
+
querySessionStatus: async (_args) => null,
|
|
1096
|
+
revokeTokens: async (_types) => void 0,
|
|
1097
|
+
startSilentRenew: () => void 0,
|
|
1098
|
+
stopSilentRenew: () => void 0,
|
|
1099
|
+
user: null,
|
|
1100
|
+
isLoading: false,
|
|
1101
|
+
isAuthenticated: false,
|
|
1102
|
+
activeNavigator: void 0,
|
|
1103
|
+
error: void 0
|
|
1104
|
+
};
|
|
880
1105
|
function isZeroConfig(props) {
|
|
881
1106
|
return "authority" in props && "clientId" in props;
|
|
882
1107
|
}
|
|
883
1108
|
var KeycloakProvider = (props) => {
|
|
884
1109
|
const { children, debug = false } = props;
|
|
1110
|
+
const status = react.useMemo(() => {
|
|
1111
|
+
const enabledFlag = props.enabled ?? true;
|
|
1112
|
+
const hasAuthority = isZeroConfig(props) ? Boolean(props.authority) : Boolean(props.config.authority);
|
|
1113
|
+
const hasClientId = isZeroConfig(props) ? Boolean(props.clientId) : Boolean(props.config.clientId);
|
|
1114
|
+
return {
|
|
1115
|
+
isEnabled: enabledFlag,
|
|
1116
|
+
isConfigured: hasAuthority && hasClientId,
|
|
1117
|
+
reason: !enabledFlag ? "disabled" : !hasAuthority || !hasClientId ? "missing-config" : void 0
|
|
1118
|
+
};
|
|
1119
|
+
}, [props]);
|
|
1120
|
+
const effectiveEnabled = status.isEnabled && status.isConfigured;
|
|
1121
|
+
react.useEffect(() => {
|
|
1122
|
+
if (!effectiveEnabled && props.tokenBridge) {
|
|
1123
|
+
props.tokenBridge.setToken(null);
|
|
1124
|
+
}
|
|
1125
|
+
}, [effectiveEnabled, props.tokenBridge]);
|
|
1126
|
+
if (!effectiveEnabled) {
|
|
1127
|
+
return /* @__PURE__ */ jsxRuntime.jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsxRuntime.jsx(reactOidcContext.AuthContext.Provider, { value: disabledAuthContext, children: /* @__PURE__ */ jsxRuntime.jsx(StorageConfigProvider, { storageType: "session", children }) }) });
|
|
1128
|
+
}
|
|
885
1129
|
const config = isZeroConfig(props) ? buildZeroConfig(props) : props.config;
|
|
886
1130
|
const oidcConfig = KeycloakConfigBuilder.build(config);
|
|
887
1131
|
const log = react.useCallback(
|
|
@@ -923,7 +1167,7 @@ var KeycloakProvider = (props) => {
|
|
|
923
1167
|
}, [log]);
|
|
924
1168
|
const showSessionMonitor = config.features?.sessionMonitor !== false;
|
|
925
1169
|
const storageType = config.advanced?.storageType || "session";
|
|
926
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1170
|
+
return /* @__PURE__ */ jsxRuntime.jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
927
1171
|
reactOidcContext.AuthProvider,
|
|
928
1172
|
{
|
|
929
1173
|
...oidcConfig,
|
|
@@ -931,11 +1175,12 @@ var KeycloakProvider = (props) => {
|
|
|
931
1175
|
onSignoutCallback,
|
|
932
1176
|
onRemoveUser,
|
|
933
1177
|
children: /* @__PURE__ */ jsxRuntime.jsx(StorageConfigProvider, { storageType, children: /* @__PURE__ */ jsxRuntime.jsxs(TokenLifecycleManager, { config, debug, children: [
|
|
1178
|
+
props.tokenBridge && /* @__PURE__ */ jsxRuntime.jsx(TokenBridgeSync, { bridge: props.tokenBridge }),
|
|
934
1179
|
showSessionMonitor && /* @__PURE__ */ jsxRuntime.jsx(SessionMonitor, {}),
|
|
935
1180
|
children
|
|
936
1181
|
] }) })
|
|
937
1182
|
}
|
|
938
|
-
);
|
|
1183
|
+
) });
|
|
939
1184
|
};
|
|
940
1185
|
function buildZeroConfig(props) {
|
|
941
1186
|
const { authority, clientId, preset = "default", config: overrides = {} } = props;
|
|
@@ -1643,13 +1888,18 @@ exports.CONFIG_PRESETS = CONFIG_PRESETS;
|
|
|
1643
1888
|
exports.DeviceLogin = DeviceLogin;
|
|
1644
1889
|
exports.KeycloakConfigBuilder = KeycloakConfigBuilder;
|
|
1645
1890
|
exports.KeycloakProvider = KeycloakProvider;
|
|
1891
|
+
exports.KeycloakStatusProvider = KeycloakStatusProvider;
|
|
1646
1892
|
exports.LoginButton = LoginButton;
|
|
1647
1893
|
exports.LogoutButton = LogoutButton;
|
|
1648
1894
|
exports.ProtectedRoute = ProtectedRoute;
|
|
1649
1895
|
exports.SessionMonitor = SessionMonitor;
|
|
1896
|
+
exports.TokenBridge = TokenBridge;
|
|
1897
|
+
exports.TokenBridgeSync = TokenBridgeSync;
|
|
1650
1898
|
exports.TokenLifecycleManager = TokenLifecycleManager;
|
|
1651
1899
|
exports.UserProfile = UserProfile;
|
|
1900
|
+
exports.createTokenBridge = createTokenBridge;
|
|
1652
1901
|
exports.defaultConfig = defaultConfig;
|
|
1902
|
+
exports.defaultTokenBridge = defaultTokenBridge;
|
|
1653
1903
|
exports.fetchUserInfo = fetchUserInfo;
|
|
1654
1904
|
exports.getConfigFromEnv = getConfigFromEnv;
|
|
1655
1905
|
exports.getConfigPreset = getConfigPreset;
|
|
@@ -1657,6 +1907,7 @@ exports.mergeWithPreset = mergeWithPreset;
|
|
|
1657
1907
|
exports.pollForToken = pollForToken;
|
|
1658
1908
|
exports.requestDeviceCode = requestDeviceCode;
|
|
1659
1909
|
exports.useAuth = useAuth;
|
|
1910
|
+
exports.useKeycloakStatus = useKeycloakStatus;
|
|
1660
1911
|
exports.useProtectedFetch = useProtectedFetch;
|
|
1661
1912
|
exports.useRoles = useRoles;
|
|
1662
1913
|
exports.useScopes = useScopes;
|