antaeus.keycloak.react 2.2.2 → 2.3.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 +18 -1
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +50 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -325,6 +325,24 @@ If your refresh token expires (exceeds `maxRefreshTokenLifetime`):
|
|
|
325
325
|
/>
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
+
### Refresh Tokens After Restart
|
|
329
|
+
|
|
330
|
+
When a refresh token is available the provider now reuses it on startup, ensuring long-lived sessions survive browser reloads or app restarts. The refresh token grant is attempted automatically before falling back to iframe-based silent renew.
|
|
331
|
+
|
|
332
|
+
If you need to disable this behavior and revert to iframe-only silent renew, set:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
<KeycloakProvider
|
|
336
|
+
authority="..."
|
|
337
|
+
clientId="my-app"
|
|
338
|
+
config={{
|
|
339
|
+
silentRenew: {
|
|
340
|
+
useRefreshToken: false,
|
|
341
|
+
},
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
```
|
|
345
|
+
|
|
328
346
|
### Startup Token Refresh
|
|
329
347
|
|
|
330
348
|
When your app loads with an existing session:
|
|
@@ -446,4 +464,3 @@ const { fetchJson } = useProtectedFetch({
|
|
|
446
464
|
## License
|
|
447
465
|
|
|
448
466
|
MIT � Saeed Aghdam
|
|
449
|
-
|
package/dist/index.d.mts
CHANGED
|
@@ -113,6 +113,12 @@ interface SilentRenewConfig {
|
|
|
113
113
|
* @default 'Refreshing session...'
|
|
114
114
|
*/
|
|
115
115
|
loadingMessage?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Enable using the refresh token grant instead of iframe silent renew when available.
|
|
118
|
+
* Keeps long-lived sessions alive even after the app is closed.
|
|
119
|
+
* @default true
|
|
120
|
+
*/
|
|
121
|
+
useRefreshToken?: boolean;
|
|
116
122
|
}
|
|
117
123
|
/**
|
|
118
124
|
* Event callbacks for authentication lifecycle
|
|
@@ -318,6 +324,7 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
318
324
|
* silentRenew: {
|
|
319
325
|
* maxRefreshTokenLifetime: 604800, // Override: 7 days instead of 30
|
|
320
326
|
* showLoadingIndicator: true,
|
|
327
|
+
* useRefreshToken: true, // Explicitly ensure refresh tokens are used when available
|
|
321
328
|
* }
|
|
322
329
|
* }}
|
|
323
330
|
* debug={true}
|
|
@@ -361,6 +368,7 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
361
368
|
* enabled: true,
|
|
362
369
|
* refreshStrategy: 'both',
|
|
363
370
|
* maxRefreshTokenLifetime: 2592000,
|
|
371
|
+
* useRefreshToken: true,
|
|
364
372
|
* },
|
|
365
373
|
* events: {
|
|
366
374
|
* onLogin: (user) => console.log('User logged in', user),
|
|
@@ -380,6 +388,7 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
380
388
|
* - ✅ Token lifecycle management (refresh on app startup)
|
|
381
389
|
* - ✅ Session monitoring UI (countdown timer)
|
|
382
390
|
* - ✅ Silent renew (background token refresh)
|
|
391
|
+
* - ✅ Refresh token grant fallback on app restart
|
|
383
392
|
* - ✅ Error notifications (user-friendly messages)
|
|
384
393
|
* - ✅ 401 retry logic (automatic token refresh on API errors)
|
|
385
394
|
* - ✅ Maximum refresh token lifetime (security control)
|
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ interface SilentRenewConfig {
|
|
|
113
113
|
* @default 'Refreshing session...'
|
|
114
114
|
*/
|
|
115
115
|
loadingMessage?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Enable using the refresh token grant instead of iframe silent renew when available.
|
|
118
|
+
* Keeps long-lived sessions alive even after the app is closed.
|
|
119
|
+
* @default true
|
|
120
|
+
*/
|
|
121
|
+
useRefreshToken?: boolean;
|
|
116
122
|
}
|
|
117
123
|
/**
|
|
118
124
|
* Event callbacks for authentication lifecycle
|
|
@@ -318,6 +324,7 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
318
324
|
* silentRenew: {
|
|
319
325
|
* maxRefreshTokenLifetime: 604800, // Override: 7 days instead of 30
|
|
320
326
|
* showLoadingIndicator: true,
|
|
327
|
+
* useRefreshToken: true, // Explicitly ensure refresh tokens are used when available
|
|
321
328
|
* }
|
|
322
329
|
* }}
|
|
323
330
|
* debug={true}
|
|
@@ -361,6 +368,7 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
361
368
|
* enabled: true,
|
|
362
369
|
* refreshStrategy: 'both',
|
|
363
370
|
* maxRefreshTokenLifetime: 2592000,
|
|
371
|
+
* useRefreshToken: true,
|
|
364
372
|
* },
|
|
365
373
|
* events: {
|
|
366
374
|
* onLogin: (user) => console.log('User logged in', user),
|
|
@@ -380,6 +388,7 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
380
388
|
* - ✅ Token lifecycle management (refresh on app startup)
|
|
381
389
|
* - ✅ Session monitoring UI (countdown timer)
|
|
382
390
|
* - ✅ Silent renew (background token refresh)
|
|
391
|
+
* - ✅ Refresh token grant fallback on app restart
|
|
383
392
|
* - ✅ Error notifications (user-friendly messages)
|
|
384
393
|
* - ✅ 401 retry logic (automatic token refresh on API errors)
|
|
385
394
|
* - ✅ Maximum refresh token lifetime (security control)
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,8 @@ var defaultConfig = {
|
|
|
30
30
|
maxRefreshTokenLifetime: 2592e3,
|
|
31
31
|
// 30 days in seconds
|
|
32
32
|
showLoadingIndicator: false,
|
|
33
|
-
loadingMessage: "Refreshing session..."
|
|
33
|
+
loadingMessage: "Refreshing session...",
|
|
34
|
+
useRefreshToken: true
|
|
34
35
|
},
|
|
35
36
|
advanced: {
|
|
36
37
|
responseType: "code",
|
|
@@ -56,6 +57,7 @@ var KeycloakConfigBuilder = class {
|
|
|
56
57
|
const config = this.mergeWithDefaults(userConfig);
|
|
57
58
|
const storageType = config.advanced.storageType || "session";
|
|
58
59
|
const storage = storageType === "local" ? window.localStorage : window.sessionStorage;
|
|
60
|
+
const { useRefreshToken } = config.silentRenew;
|
|
59
61
|
const oidcSettings = {
|
|
60
62
|
authority: config.authority,
|
|
61
63
|
client_id: config.clientId,
|
|
@@ -73,6 +75,9 @@ var KeycloakConfigBuilder = class {
|
|
|
73
75
|
response_mode: "query"
|
|
74
76
|
}
|
|
75
77
|
};
|
|
78
|
+
if (useRefreshToken !== void 0) {
|
|
79
|
+
oidcSettings.useRefreshToken = useRefreshToken;
|
|
80
|
+
}
|
|
76
81
|
return oidcSettings;
|
|
77
82
|
}
|
|
78
83
|
/**
|
|
@@ -160,6 +165,7 @@ function useTokenLifecycle(options) {
|
|
|
160
165
|
const [isRefreshing, setIsRefreshing] = react.useState(false);
|
|
161
166
|
const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
|
|
162
167
|
const hasCheckedOnStartup = react.useRef(false);
|
|
168
|
+
const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
|
|
163
169
|
const log = react.useCallback(
|
|
164
170
|
(message, ...args) => {
|
|
165
171
|
if (debug) {
|
|
@@ -296,24 +302,49 @@ function useTokenLifecycle(options) {
|
|
|
296
302
|
react.useEffect(() => {
|
|
297
303
|
const strategy = config.silentRenew?.refreshStrategy || "both";
|
|
298
304
|
const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
|
|
305
|
+
const hasRefreshToken = Boolean(auth.user?.refresh_token);
|
|
299
306
|
if (!shouldCheckOnStartup || hasCheckedOnStartup.current) {
|
|
300
307
|
return;
|
|
301
308
|
}
|
|
302
|
-
if (
|
|
309
|
+
if (auth.isLoading) {
|
|
303
310
|
return;
|
|
304
311
|
}
|
|
312
|
+
if (!auth.isAuthenticated) {
|
|
313
|
+
if (!supportsRefreshTokens) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!auth.user || !hasRefreshToken) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
305
320
|
hasCheckedOnStartup.current = true;
|
|
306
|
-
log(
|
|
321
|
+
log(
|
|
322
|
+
"Running startup token check with strategy:",
|
|
323
|
+
strategy,
|
|
324
|
+
"using refresh token:",
|
|
325
|
+
!auth.isAuthenticated && supportsRefreshTokens
|
|
326
|
+
);
|
|
307
327
|
checkAndRefreshTokens().catch((error) => {
|
|
308
328
|
log("Startup token check failed:", error);
|
|
309
329
|
});
|
|
310
330
|
}, [
|
|
311
331
|
auth.isAuthenticated,
|
|
312
332
|
auth.isLoading,
|
|
333
|
+
auth.user,
|
|
313
334
|
config.silentRenew?.refreshStrategy,
|
|
335
|
+
supportsRefreshTokens,
|
|
314
336
|
checkAndRefreshTokens,
|
|
315
337
|
log
|
|
316
338
|
]);
|
|
339
|
+
react.useEffect(() => {
|
|
340
|
+
if (hasCheckedOnStartup.current) {
|
|
341
|
+
const hasUser = Boolean(auth.user);
|
|
342
|
+
const hasRefreshToken = Boolean(auth.user?.refresh_token);
|
|
343
|
+
if (auth.isLoading || !auth.isAuthenticated && (!supportsRefreshTokens || !hasUser || !hasRefreshToken)) {
|
|
344
|
+
hasCheckedOnStartup.current = false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}, [auth.isAuthenticated, auth.isLoading, auth.user, supportsRefreshTokens]);
|
|
317
348
|
react.useEffect(() => {
|
|
318
349
|
if (auth.user && !auth.isLoading) {
|
|
319
350
|
storeFirstLoginTime();
|
|
@@ -837,6 +868,15 @@ function mergeWithPreset(preset, customConfig) {
|
|
|
837
868
|
}
|
|
838
869
|
};
|
|
839
870
|
}
|
|
871
|
+
var StorageConfigContext = react.createContext({
|
|
872
|
+
storageType: "session",
|
|
873
|
+
storage: typeof window !== "undefined" ? window.sessionStorage : {}
|
|
874
|
+
});
|
|
875
|
+
var StorageConfigProvider = ({ storageType, children }) => {
|
|
876
|
+
const storage = storageType === "local" ? window.localStorage : window.sessionStorage;
|
|
877
|
+
return /* @__PURE__ */ jsxRuntime.jsx(StorageConfigContext.Provider, { value: { storageType, storage }, children });
|
|
878
|
+
};
|
|
879
|
+
var useStorageConfig = () => react.useContext(StorageConfigContext);
|
|
840
880
|
function isZeroConfig(props) {
|
|
841
881
|
return "authority" in props && "clientId" in props;
|
|
842
882
|
}
|
|
@@ -882,6 +922,7 @@ var KeycloakProvider = (props) => {
|
|
|
882
922
|
log("warn", "User removed from storage - token validation likely failed");
|
|
883
923
|
}, [log]);
|
|
884
924
|
const showSessionMonitor = config.features?.sessionMonitor !== false;
|
|
925
|
+
const storageType = config.advanced?.storageType || "session";
|
|
885
926
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
886
927
|
reactOidcContext.AuthProvider,
|
|
887
928
|
{
|
|
@@ -889,10 +930,10 @@ var KeycloakProvider = (props) => {
|
|
|
889
930
|
onSigninCallback,
|
|
890
931
|
onSignoutCallback,
|
|
891
932
|
onRemoveUser,
|
|
892
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs(TokenLifecycleManager, { config, debug, children: [
|
|
933
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(StorageConfigProvider, { storageType, children: /* @__PURE__ */ jsxRuntime.jsxs(TokenLifecycleManager, { config, debug, children: [
|
|
893
934
|
showSessionMonitor && /* @__PURE__ */ jsxRuntime.jsx(SessionMonitor, {}),
|
|
894
935
|
children
|
|
895
|
-
] })
|
|
936
|
+
] }) })
|
|
896
937
|
}
|
|
897
938
|
);
|
|
898
939
|
};
|
|
@@ -1349,6 +1390,7 @@ var DeviceLogin = ({
|
|
|
1349
1390
|
style
|
|
1350
1391
|
}) => {
|
|
1351
1392
|
const auth = useAuth();
|
|
1393
|
+
const { storage, storageType } = useStorageConfig();
|
|
1352
1394
|
const [deviceData, setDeviceData] = react.useState(null);
|
|
1353
1395
|
const [error, setError] = react.useState(null);
|
|
1354
1396
|
const [loading, setLoading] = react.useState(true);
|
|
@@ -1415,10 +1457,10 @@ var DeviceLogin = ({
|
|
|
1415
1457
|
});
|
|
1416
1458
|
const storageKey = `oidc.user:${auth.settings.authority}:${auth.settings.client_id}`;
|
|
1417
1459
|
const userJson = user.toStorageString();
|
|
1418
|
-
const storage = auth.settings.userStore?.store || window.sessionStorage;
|
|
1419
1460
|
console.log("[DeviceLogin] Storing user in storage...", {
|
|
1420
1461
|
storageKey,
|
|
1421
|
-
storageType
|
|
1462
|
+
storageType,
|
|
1463
|
+
storage: storageType === "local" ? "localStorage" : "sessionStorage"
|
|
1422
1464
|
});
|
|
1423
1465
|
storage.setItem(storageKey, userJson);
|
|
1424
1466
|
const stored = storage.getItem(storageKey);
|
|
@@ -1426,7 +1468,7 @@ var DeviceLogin = ({
|
|
|
1426
1468
|
console.error("[DeviceLogin] Failed to store user in storage!");
|
|
1427
1469
|
throw new Error("Failed to store user session. Please try again.");
|
|
1428
1470
|
}
|
|
1429
|
-
console.log("[DeviceLogin] User stored successfully in",
|
|
1471
|
+
console.log("[DeviceLogin] User stored successfully in", storageType === "local" ? "localStorage" : "sessionStorage");
|
|
1430
1472
|
if (onSuccess) {
|
|
1431
1473
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1432
1474
|
onSuccess(user);
|