antaeus.keycloak.react 2.7.0 → 2.7.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/README.md +21 -0
- package/dist/index.d.mts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +58 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,6 +155,27 @@ const { isEnabled, isConfigured, reason } = useKeycloakStatus();
|
|
|
155
155
|
|
|
156
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
157
|
|
|
158
|
+
### Silent Renewal Fallback
|
|
159
|
+
|
|
160
|
+
`oidc-client-ts` always uses a stored refresh token when one is available. To permit iframe renewal only when no refresh token is stored, opt in explicitly:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<KeycloakProvider
|
|
164
|
+
authority="https://sso.example.com/realms/myrealm"
|
|
165
|
+
clientId="my-app"
|
|
166
|
+
config={{
|
|
167
|
+
silentRenew: {
|
|
168
|
+
allowIframeFallback: true,
|
|
169
|
+
transientRetry: { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 30000 },
|
|
170
|
+
},
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<App />
|
|
174
|
+
</KeycloakProvider>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The application must host `silentRenew.silentRedirectUri` (default `/silent-renew.html`), register its exact URL with the identity provider, and allow the identity provider to load in an iframe. Browser privacy settings and third-party-cookie policy can still prevent iframe renewal. `useRefreshToken` remains for source compatibility but cannot force iframe-only renewal.
|
|
178
|
+
|
|
158
179
|
## React Hooks
|
|
159
180
|
|
|
160
181
|
### useAuth()
|
package/dist/index.d.mts
CHANGED
|
@@ -116,11 +116,23 @@ interface SilentRenewConfig {
|
|
|
116
116
|
*/
|
|
117
117
|
loadingMessage?: string;
|
|
118
118
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
119
|
+
* Legacy compatibility setting. oidc-client-ts automatically uses a stored
|
|
120
|
+
* refresh token when one is available.
|
|
121
121
|
* @default true
|
|
122
122
|
*/
|
|
123
123
|
useRefreshToken?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Allow oidc-client-ts to use iframe silent renewal when no refresh token is stored.
|
|
126
|
+
* The application must host the configured silentRedirectUri and permit it in the
|
|
127
|
+
* identity provider redirect URI allowlist.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
allowIframeFallback?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Retry policy for transient online silent-renew failures.
|
|
133
|
+
* Offline renewals wait for the browser's online event instead of using this policy.
|
|
134
|
+
*/
|
|
135
|
+
transientRetry?: TransientRetryConfig;
|
|
124
136
|
/**
|
|
125
137
|
* Re-check token freshness when a suspended tab/PWA resumes.
|
|
126
138
|
* Useful for mobile browsers and installed PWAs where timers pause in the background.
|
|
@@ -128,6 +140,14 @@ interface SilentRenewConfig {
|
|
|
128
140
|
*/
|
|
129
141
|
refreshOnResume?: boolean;
|
|
130
142
|
}
|
|
143
|
+
interface TransientRetryConfig {
|
|
144
|
+
/** Maximum automatic retries after a transient online failure. @default 3 */
|
|
145
|
+
maxAttempts?: number;
|
|
146
|
+
/** Delay before the first retry in milliseconds. @default 1000 */
|
|
147
|
+
initialDelayMs?: number;
|
|
148
|
+
/** Maximum exponential backoff delay in milliseconds. @default 30000 */
|
|
149
|
+
maxDelayMs?: number;
|
|
150
|
+
}
|
|
131
151
|
/**
|
|
132
152
|
* Event callbacks for authentication lifecycle
|
|
133
153
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -116,11 +116,23 @@ interface SilentRenewConfig {
|
|
|
116
116
|
*/
|
|
117
117
|
loadingMessage?: string;
|
|
118
118
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
119
|
+
* Legacy compatibility setting. oidc-client-ts automatically uses a stored
|
|
120
|
+
* refresh token when one is available.
|
|
121
121
|
* @default true
|
|
122
122
|
*/
|
|
123
123
|
useRefreshToken?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Allow oidc-client-ts to use iframe silent renewal when no refresh token is stored.
|
|
126
|
+
* The application must host the configured silentRedirectUri and permit it in the
|
|
127
|
+
* identity provider redirect URI allowlist.
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
allowIframeFallback?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Retry policy for transient online silent-renew failures.
|
|
133
|
+
* Offline renewals wait for the browser's online event instead of using this policy.
|
|
134
|
+
*/
|
|
135
|
+
transientRetry?: TransientRetryConfig;
|
|
124
136
|
/**
|
|
125
137
|
* Re-check token freshness when a suspended tab/PWA resumes.
|
|
126
138
|
* Useful for mobile browsers and installed PWAs where timers pause in the background.
|
|
@@ -128,6 +140,14 @@ interface SilentRenewConfig {
|
|
|
128
140
|
*/
|
|
129
141
|
refreshOnResume?: boolean;
|
|
130
142
|
}
|
|
143
|
+
interface TransientRetryConfig {
|
|
144
|
+
/** Maximum automatic retries after a transient online failure. @default 3 */
|
|
145
|
+
maxAttempts?: number;
|
|
146
|
+
/** Delay before the first retry in milliseconds. @default 1000 */
|
|
147
|
+
initialDelayMs?: number;
|
|
148
|
+
/** Maximum exponential backoff delay in milliseconds. @default 30000 */
|
|
149
|
+
maxDelayMs?: number;
|
|
150
|
+
}
|
|
131
151
|
/**
|
|
132
152
|
* Event callbacks for authentication lifecycle
|
|
133
153
|
*/
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,12 @@ var defaultConfig = {
|
|
|
37
37
|
showLoadingIndicator: false,
|
|
38
38
|
loadingMessage: "Refreshing session...",
|
|
39
39
|
useRefreshToken: true,
|
|
40
|
+
allowIframeFallback: false,
|
|
41
|
+
transientRetry: {
|
|
42
|
+
maxAttempts: 3,
|
|
43
|
+
initialDelayMs: 1e3,
|
|
44
|
+
maxDelayMs: 3e4
|
|
45
|
+
},
|
|
40
46
|
refreshOnResume: true
|
|
41
47
|
},
|
|
42
48
|
advanced: {
|
|
@@ -63,7 +69,6 @@ var KeycloakConfigBuilder = class {
|
|
|
63
69
|
const config = this.mergeWithDefaults(userConfig);
|
|
64
70
|
const storageType = config.advanced.storageType || "session";
|
|
65
71
|
const storage = storageType === "local" ? window.localStorage : window.sessionStorage;
|
|
66
|
-
const { useRefreshToken } = config.silentRenew;
|
|
67
72
|
const oidcSettings = {
|
|
68
73
|
authority: config.authority,
|
|
69
74
|
client_id: config.clientId,
|
|
@@ -81,9 +86,6 @@ var KeycloakConfigBuilder = class {
|
|
|
81
86
|
response_mode: "query"
|
|
82
87
|
}
|
|
83
88
|
};
|
|
84
|
-
if (useRefreshToken !== void 0) {
|
|
85
|
-
oidcSettings.useRefreshToken = useRefreshToken;
|
|
86
|
-
}
|
|
87
89
|
return oidcSettings;
|
|
88
90
|
}
|
|
89
91
|
/**
|
|
@@ -158,6 +160,9 @@ var useAuth = reactOidcContext.useAuth;
|
|
|
158
160
|
|
|
159
161
|
// src/hooks/useTokenLifecycle.ts
|
|
160
162
|
var STORAGE_KEY_PREFIX = "keycloak_first_login_";
|
|
163
|
+
var DEFAULT_MAX_TRANSIENT_RETRIES = 3;
|
|
164
|
+
var DEFAULT_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
165
|
+
var DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
|
|
161
166
|
var isNavigatorOffline = () => typeof navigator !== "undefined" && "onLine" in navigator && navigator.onLine === false;
|
|
162
167
|
var isTransientNetworkError = (error) => {
|
|
163
168
|
const message = error.message?.toLowerCase?.() ?? "";
|
|
@@ -193,13 +198,34 @@ function useTokenLifecycle(options) {
|
|
|
193
198
|
const [isRefreshing, setIsRefreshing] = react.useState(false);
|
|
194
199
|
const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
|
|
195
200
|
const isRefreshingRef = react.useRef(false);
|
|
201
|
+
const isMountedRef = react.useRef(true);
|
|
196
202
|
const hasCheckedOnStartup = react.useRef(false);
|
|
197
|
-
const
|
|
203
|
+
const allowIframeFallback = config.silentRenew?.allowIframeFallback === true;
|
|
198
204
|
const [pendingRetry, setPendingRetry] = react.useState(null);
|
|
199
205
|
const deferredRefreshReasonRef = react.useRef(null);
|
|
206
|
+
const transientRetryAttemptsRef = react.useRef(0);
|
|
200
207
|
const enqueueRetry = react.useCallback((reason) => {
|
|
201
208
|
setPendingRetry((current) => current === reason ? current : reason);
|
|
202
209
|
}, []);
|
|
210
|
+
const transientRetry = config.silentRenew?.transientRetry;
|
|
211
|
+
const maxTransientRetries = Math.max(
|
|
212
|
+
0,
|
|
213
|
+
transientRetry?.maxAttempts ?? DEFAULT_MAX_TRANSIENT_RETRIES
|
|
214
|
+
);
|
|
215
|
+
const retryInitialDelayMs = Math.max(
|
|
216
|
+
0,
|
|
217
|
+
transientRetry?.initialDelayMs ?? DEFAULT_RETRY_INITIAL_DELAY_MS
|
|
218
|
+
);
|
|
219
|
+
const retryMaxDelayMs = Math.max(
|
|
220
|
+
retryInitialDelayMs,
|
|
221
|
+
transientRetry?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS
|
|
222
|
+
);
|
|
223
|
+
react.useEffect(() => {
|
|
224
|
+
isMountedRef.current = true;
|
|
225
|
+
return () => {
|
|
226
|
+
isMountedRef.current = false;
|
|
227
|
+
};
|
|
228
|
+
}, []);
|
|
203
229
|
const log = react.useCallback(
|
|
204
230
|
(message, ...args) => {
|
|
205
231
|
if (debug) {
|
|
@@ -269,6 +295,7 @@ function useTokenLifecycle(options) {
|
|
|
269
295
|
log("Starting token refresh");
|
|
270
296
|
await auth.signinSilent();
|
|
271
297
|
log("Token refresh successful");
|
|
298
|
+
transientRetryAttemptsRef.current = 0;
|
|
272
299
|
onRefreshSuccess?.();
|
|
273
300
|
} catch (error) {
|
|
274
301
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -291,7 +318,15 @@ function useTokenLifecycle(options) {
|
|
|
291
318
|
deferredRefreshReasonRef.current = reason;
|
|
292
319
|
if (reason === "offline") {
|
|
293
320
|
log("Token refresh deferred because device is offline");
|
|
321
|
+
transientRetryAttemptsRef.current = 0;
|
|
294
322
|
} else {
|
|
323
|
+
if (transientRetryAttemptsRef.current >= maxTransientRetries) {
|
|
324
|
+
deferredRefreshReasonRef.current = null;
|
|
325
|
+
log("Token refresh retry limit reached:", err.message);
|
|
326
|
+
onRefreshError?.(err);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
transientRetryAttemptsRef.current += 1;
|
|
295
330
|
log(
|
|
296
331
|
"Token refresh failed due to transient network issue, will retry automatically:",
|
|
297
332
|
err.message
|
|
@@ -317,7 +352,8 @@ function useTokenLifecycle(options) {
|
|
|
317
352
|
onRefreshTokenExpired,
|
|
318
353
|
enqueueRetry,
|
|
319
354
|
getStorageKey,
|
|
320
|
-
log
|
|
355
|
+
log,
|
|
356
|
+
maxTransientRetries
|
|
321
357
|
]);
|
|
322
358
|
const shouldRefreshOnResume = react.useCallback((reason) => {
|
|
323
359
|
const silentRenewEnabled = config.silentRenew?.enabled !== false;
|
|
@@ -334,7 +370,7 @@ function useTokenLifecycle(options) {
|
|
|
334
370
|
if (!auth.user) {
|
|
335
371
|
return false;
|
|
336
372
|
}
|
|
337
|
-
if (
|
|
373
|
+
if (!hasRefreshToken && !allowIframeFallback) {
|
|
338
374
|
log("Skipping resume token check because no refresh token is available:", reason);
|
|
339
375
|
return false;
|
|
340
376
|
}
|
|
@@ -348,7 +384,7 @@ function useTokenLifecycle(options) {
|
|
|
348
384
|
config.silentRenew?.enabled,
|
|
349
385
|
config.silentRenew?.refreshOnResume,
|
|
350
386
|
config.silentRenew?.refreshStrategy,
|
|
351
|
-
|
|
387
|
+
allowIframeFallback,
|
|
352
388
|
log
|
|
353
389
|
]);
|
|
354
390
|
const checkAndRefreshTokens = react.useCallback(async () => {
|
|
@@ -421,10 +457,10 @@ function useTokenLifecycle(options) {
|
|
|
421
457
|
}
|
|
422
458
|
let cancelled = false;
|
|
423
459
|
const executeRetry = () => {
|
|
424
|
-
if (
|
|
460
|
+
if (!isMountedRef.current) {
|
|
425
461
|
return;
|
|
426
462
|
}
|
|
427
|
-
if (
|
|
463
|
+
if (isRefreshingRef.current) {
|
|
428
464
|
log("Skipping scheduled token refresh retry because a refresh is already in progress");
|
|
429
465
|
return;
|
|
430
466
|
}
|
|
@@ -448,19 +484,23 @@ function useTokenLifecycle(options) {
|
|
|
448
484
|
window.removeEventListener("online", handleOnline);
|
|
449
485
|
};
|
|
450
486
|
}
|
|
487
|
+
const retryDelay = Math.min(
|
|
488
|
+
retryInitialDelayMs * 2 ** Math.max(0, transientRetryAttemptsRef.current - 1),
|
|
489
|
+
retryMaxDelayMs
|
|
490
|
+
);
|
|
451
491
|
const timeoutId = window.setTimeout(() => {
|
|
452
492
|
if (cancelled) {
|
|
453
493
|
return;
|
|
454
494
|
}
|
|
455
495
|
log("Retrying token refresh after transient network failure delay");
|
|
456
496
|
setPendingRetry(null);
|
|
457
|
-
executeRetry
|
|
458
|
-
},
|
|
497
|
+
window.setTimeout(executeRetry, 0);
|
|
498
|
+
}, retryDelay);
|
|
459
499
|
return () => {
|
|
460
500
|
cancelled = true;
|
|
461
501
|
window.clearTimeout(timeoutId);
|
|
462
502
|
};
|
|
463
|
-
}, [pendingRetry, refreshToken,
|
|
503
|
+
}, [pendingRetry, refreshToken, log, retryInitialDelayMs, retryMaxDelayMs]);
|
|
464
504
|
react.useEffect(() => {
|
|
465
505
|
const strategy = config.silentRenew?.refreshStrategy || "both";
|
|
466
506
|
const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
|
|
@@ -472,10 +512,7 @@ function useTokenLifecycle(options) {
|
|
|
472
512
|
return;
|
|
473
513
|
}
|
|
474
514
|
if (!auth.isAuthenticated) {
|
|
475
|
-
if (!
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
if (!auth.user || !hasRefreshToken) {
|
|
515
|
+
if (!auth.user || !hasRefreshToken && !allowIframeFallback) {
|
|
479
516
|
return;
|
|
480
517
|
}
|
|
481
518
|
}
|
|
@@ -484,7 +521,7 @@ function useTokenLifecycle(options) {
|
|
|
484
521
|
"Running startup token check with strategy:",
|
|
485
522
|
strategy,
|
|
486
523
|
"using refresh token:",
|
|
487
|
-
!auth.isAuthenticated &&
|
|
524
|
+
!auth.isAuthenticated && hasRefreshToken
|
|
488
525
|
);
|
|
489
526
|
checkAndRefreshTokens().catch((error) => {
|
|
490
527
|
log("Startup token check failed:", error);
|
|
@@ -494,7 +531,7 @@ function useTokenLifecycle(options) {
|
|
|
494
531
|
auth.isLoading,
|
|
495
532
|
auth.user,
|
|
496
533
|
config.silentRenew?.refreshStrategy,
|
|
497
|
-
|
|
534
|
+
allowIframeFallback,
|
|
498
535
|
checkAndRefreshTokens,
|
|
499
536
|
log
|
|
500
537
|
]);
|
|
@@ -534,11 +571,11 @@ function useTokenLifecycle(options) {
|
|
|
534
571
|
if (hasCheckedOnStartup.current) {
|
|
535
572
|
const hasUser = Boolean(auth.user);
|
|
536
573
|
const hasRefreshToken = Boolean(auth.user?.refresh_token);
|
|
537
|
-
if (!auth.isAuthenticated && (!
|
|
574
|
+
if (!auth.isAuthenticated && (!hasUser || !hasRefreshToken && !allowIframeFallback)) {
|
|
538
575
|
hasCheckedOnStartup.current = false;
|
|
539
576
|
}
|
|
540
577
|
}
|
|
541
|
-
}, [auth.isAuthenticated, auth.user,
|
|
578
|
+
}, [auth.isAuthenticated, auth.user, allowIframeFallback]);
|
|
542
579
|
react.useEffect(() => {
|
|
543
580
|
if (auth.user && !auth.isLoading) {
|
|
544
581
|
storeFirstLoginTime();
|