antaeus.keycloak.react 2.6.4 → 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 +70 -0
- package/dist/index.d.mts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +116 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -22
- 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()
|
|
@@ -169,6 +190,55 @@ auth.signinRedirect()
|
|
|
169
190
|
auth.signoutRedirect()
|
|
170
191
|
```
|
|
171
192
|
|
|
193
|
+
### useStoredOidcSession()
|
|
194
|
+
|
|
195
|
+
Inspect the OIDC user record stored by `oidc-client-ts` without duplicating the
|
|
196
|
+
library's storage-key format in an application. This is useful when an app wants
|
|
197
|
+
to decide whether to attempt a silent session restore or show a login prompt
|
|
198
|
+
immediately.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useAuth, useStoredOidcSession } from 'antaeus.keycloak.react';
|
|
202
|
+
|
|
203
|
+
function AuthGate() {
|
|
204
|
+
const auth = useAuth();
|
|
205
|
+
const storedSession = useStoredOidcSession();
|
|
206
|
+
|
|
207
|
+
if (!auth.isAuthenticated && !storedSession.hasRefreshToken) {
|
|
208
|
+
return <button onClick={() => auth.signinRedirect()}>Sign in</button>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return <App />;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The hook reads from the same storage configured on `KeycloakProvider` via
|
|
216
|
+
`advanced.storageType`. It does not trigger network calls or change provider
|
|
217
|
+
state.
|
|
218
|
+
|
|
219
|
+
For non-React code or tests, use the exported helpers directly:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import {
|
|
223
|
+
getOidcUserStorageKey,
|
|
224
|
+
getStoredOidcSession,
|
|
225
|
+
getStoredOidcUser,
|
|
226
|
+
hasStoredOidcRefreshToken,
|
|
227
|
+
} from 'antaeus.keycloak.react';
|
|
228
|
+
|
|
229
|
+
const storageKey = getOidcUserStorageKey(authority, clientId);
|
|
230
|
+
const storedUser = getStoredOidcUser({ authority, clientId, storage: localStorage });
|
|
231
|
+
const session = getStoredOidcSession({ authority, clientId, storage: localStorage });
|
|
232
|
+
const canTrySilentRestore = hasStoredOidcRefreshToken({
|
|
233
|
+
authority,
|
|
234
|
+
clientId,
|
|
235
|
+
storage: localStorage,
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
These helpers are additive APIs. They do not alter default provider behavior,
|
|
240
|
+
token refresh, silent renew, or session monitoring.
|
|
241
|
+
|
|
172
242
|
## Token Bridge (Non-React Consumers)
|
|
173
243
|
|
|
174
244
|
Expose tokens to plain TypeScript modules (SignalR, fetch helpers, etc.):
|
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
|
*/
|
|
@@ -1441,6 +1461,31 @@ interface UseTokenLifecycleReturn {
|
|
|
1441
1461
|
*/
|
|
1442
1462
|
declare function useTokenLifecycle(options: UseTokenLifecycleOptions): UseTokenLifecycleReturn;
|
|
1443
1463
|
|
|
1464
|
+
interface StoredOidcSessionUser {
|
|
1465
|
+
access_token?: string;
|
|
1466
|
+
refresh_token?: string;
|
|
1467
|
+
expires_at?: number;
|
|
1468
|
+
[key: string]: unknown;
|
|
1469
|
+
}
|
|
1470
|
+
interface StoredOidcSessionInfo {
|
|
1471
|
+
storageKey: string | null;
|
|
1472
|
+
storedUser: StoredOidcSessionUser | null;
|
|
1473
|
+
hasStoredUser: boolean;
|
|
1474
|
+
hasRefreshToken: boolean;
|
|
1475
|
+
}
|
|
1476
|
+
interface StoredOidcSessionArgs {
|
|
1477
|
+
authority?: string;
|
|
1478
|
+
clientId?: string;
|
|
1479
|
+
storage?: Pick<Storage, 'getItem'>;
|
|
1480
|
+
}
|
|
1481
|
+
declare const getOidcUserStorageKey: (authority?: string, clientId?: string) => string | null;
|
|
1482
|
+
declare const getStoredOidcUser: ({ authority, clientId, storage, }: StoredOidcSessionArgs) => StoredOidcSessionUser | null;
|
|
1483
|
+
declare const hasStoredOidcRefreshToken: (args: StoredOidcSessionArgs) => boolean;
|
|
1484
|
+
declare const getStoredOidcSession: (args: StoredOidcSessionArgs) => StoredOidcSessionInfo;
|
|
1485
|
+
|
|
1486
|
+
type UseStoredOidcSessionReturn = StoredOidcSessionInfo;
|
|
1487
|
+
declare const useStoredOidcSession: () => UseStoredOidcSessionReturn;
|
|
1488
|
+
|
|
1444
1489
|
/**
|
|
1445
1490
|
* Options for configuring the SignalR connection
|
|
1446
1491
|
*/
|
|
@@ -1721,4 +1766,4 @@ declare function pollForToken(apiBaseUrl: string, deviceCode: string): Promise<T
|
|
|
1721
1766
|
*/
|
|
1722
1767
|
declare function fetchUserInfo(apiBaseUrl: string, accessToken: string): Promise<any>;
|
|
1723
1768
|
|
|
1724
|
-
export { type AdvancedOidcOptions, CONFIG_PRESETS, type ConfigPreset, type DeviceAuthorizationResponse, type DeviceFlowError, DeviceLogin, type DeviceLoginProps, type EventCallbacks, type FeatureFlags, type KeycloakConfig, KeycloakConfigBuilder, type KeycloakDisabledReason, KeycloakProvider, type KeycloakProviderFullConfigProps, type KeycloakProviderProps, type KeycloakProviderZeroConfigProps, type KeycloakStatus, KeycloakStatusProvider, LoginButton, type LoginButtonProps, LogoutButton, type LogoutButtonProps, type PolicyContext, type PolicyFunction, ProtectedRoute, type ProtectedRouteProps, SessionMonitor, type SessionMonitorProps, type SilentRenewConfig, TokenBridge, type TokenBridgeProps, TokenBridgeSync, TokenLifecycleManager, type TokenLifecycleManagerProps, type TokenListener, type TokenResponse, type UseProtectedFetchOptions, type UseProtectedFetchReturn, type UseRolesReturn, type UseScopesReturn, type UseSignalRConnectionOptions, type UseSignalRConnectionReturn, type UseTokenLifecycleOptions, type UseTokenLifecycleReturn, UserProfile, type UserProfileProps, createTokenBridge, defaultConfig, defaultTokenBridge, fetchUserInfo, getConfigFromEnv, getConfigPreset, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useKeycloakStatus, useProtectedFetch, useRoles, useScopes, useSignalRConnection, useTokenLifecycle };
|
|
1769
|
+
export { type AdvancedOidcOptions, CONFIG_PRESETS, type ConfigPreset, type DeviceAuthorizationResponse, type DeviceFlowError, DeviceLogin, type DeviceLoginProps, type EventCallbacks, type FeatureFlags, type KeycloakConfig, KeycloakConfigBuilder, type KeycloakDisabledReason, KeycloakProvider, type KeycloakProviderFullConfigProps, type KeycloakProviderProps, type KeycloakProviderZeroConfigProps, type KeycloakStatus, KeycloakStatusProvider, LoginButton, type LoginButtonProps, LogoutButton, type LogoutButtonProps, type PolicyContext, type PolicyFunction, ProtectedRoute, type ProtectedRouteProps, SessionMonitor, type SessionMonitorProps, type SilentRenewConfig, type StoredOidcSessionArgs, type StoredOidcSessionInfo, type StoredOidcSessionUser, TokenBridge, type TokenBridgeProps, TokenBridgeSync, TokenLifecycleManager, type TokenLifecycleManagerProps, type TokenListener, type TokenResponse, type UseProtectedFetchOptions, type UseProtectedFetchReturn, type UseRolesReturn, type UseScopesReturn, type UseSignalRConnectionOptions, type UseSignalRConnectionReturn, type UseStoredOidcSessionReturn, type UseTokenLifecycleOptions, type UseTokenLifecycleReturn, UserProfile, type UserProfileProps, createTokenBridge, defaultConfig, defaultTokenBridge, fetchUserInfo, getConfigFromEnv, getConfigPreset, getOidcUserStorageKey, getStoredOidcSession, getStoredOidcUser, hasStoredOidcRefreshToken, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useKeycloakStatus, useProtectedFetch, useRoles, useScopes, useSignalRConnection, useStoredOidcSession, useTokenLifecycle };
|
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
|
*/
|
|
@@ -1441,6 +1461,31 @@ interface UseTokenLifecycleReturn {
|
|
|
1441
1461
|
*/
|
|
1442
1462
|
declare function useTokenLifecycle(options: UseTokenLifecycleOptions): UseTokenLifecycleReturn;
|
|
1443
1463
|
|
|
1464
|
+
interface StoredOidcSessionUser {
|
|
1465
|
+
access_token?: string;
|
|
1466
|
+
refresh_token?: string;
|
|
1467
|
+
expires_at?: number;
|
|
1468
|
+
[key: string]: unknown;
|
|
1469
|
+
}
|
|
1470
|
+
interface StoredOidcSessionInfo {
|
|
1471
|
+
storageKey: string | null;
|
|
1472
|
+
storedUser: StoredOidcSessionUser | null;
|
|
1473
|
+
hasStoredUser: boolean;
|
|
1474
|
+
hasRefreshToken: boolean;
|
|
1475
|
+
}
|
|
1476
|
+
interface StoredOidcSessionArgs {
|
|
1477
|
+
authority?: string;
|
|
1478
|
+
clientId?: string;
|
|
1479
|
+
storage?: Pick<Storage, 'getItem'>;
|
|
1480
|
+
}
|
|
1481
|
+
declare const getOidcUserStorageKey: (authority?: string, clientId?: string) => string | null;
|
|
1482
|
+
declare const getStoredOidcUser: ({ authority, clientId, storage, }: StoredOidcSessionArgs) => StoredOidcSessionUser | null;
|
|
1483
|
+
declare const hasStoredOidcRefreshToken: (args: StoredOidcSessionArgs) => boolean;
|
|
1484
|
+
declare const getStoredOidcSession: (args: StoredOidcSessionArgs) => StoredOidcSessionInfo;
|
|
1485
|
+
|
|
1486
|
+
type UseStoredOidcSessionReturn = StoredOidcSessionInfo;
|
|
1487
|
+
declare const useStoredOidcSession: () => UseStoredOidcSessionReturn;
|
|
1488
|
+
|
|
1444
1489
|
/**
|
|
1445
1490
|
* Options for configuring the SignalR connection
|
|
1446
1491
|
*/
|
|
@@ -1721,4 +1766,4 @@ declare function pollForToken(apiBaseUrl: string, deviceCode: string): Promise<T
|
|
|
1721
1766
|
*/
|
|
1722
1767
|
declare function fetchUserInfo(apiBaseUrl: string, accessToken: string): Promise<any>;
|
|
1723
1768
|
|
|
1724
|
-
export { type AdvancedOidcOptions, CONFIG_PRESETS, type ConfigPreset, type DeviceAuthorizationResponse, type DeviceFlowError, DeviceLogin, type DeviceLoginProps, type EventCallbacks, type FeatureFlags, type KeycloakConfig, KeycloakConfigBuilder, type KeycloakDisabledReason, KeycloakProvider, type KeycloakProviderFullConfigProps, type KeycloakProviderProps, type KeycloakProviderZeroConfigProps, type KeycloakStatus, KeycloakStatusProvider, LoginButton, type LoginButtonProps, LogoutButton, type LogoutButtonProps, type PolicyContext, type PolicyFunction, ProtectedRoute, type ProtectedRouteProps, SessionMonitor, type SessionMonitorProps, type SilentRenewConfig, TokenBridge, type TokenBridgeProps, TokenBridgeSync, TokenLifecycleManager, type TokenLifecycleManagerProps, type TokenListener, type TokenResponse, type UseProtectedFetchOptions, type UseProtectedFetchReturn, type UseRolesReturn, type UseScopesReturn, type UseSignalRConnectionOptions, type UseSignalRConnectionReturn, type UseTokenLifecycleOptions, type UseTokenLifecycleReturn, UserProfile, type UserProfileProps, createTokenBridge, defaultConfig, defaultTokenBridge, fetchUserInfo, getConfigFromEnv, getConfigPreset, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useKeycloakStatus, useProtectedFetch, useRoles, useScopes, useSignalRConnection, useTokenLifecycle };
|
|
1769
|
+
export { type AdvancedOidcOptions, CONFIG_PRESETS, type ConfigPreset, type DeviceAuthorizationResponse, type DeviceFlowError, DeviceLogin, type DeviceLoginProps, type EventCallbacks, type FeatureFlags, type KeycloakConfig, KeycloakConfigBuilder, type KeycloakDisabledReason, KeycloakProvider, type KeycloakProviderFullConfigProps, type KeycloakProviderProps, type KeycloakProviderZeroConfigProps, type KeycloakStatus, KeycloakStatusProvider, LoginButton, type LoginButtonProps, LogoutButton, type LogoutButtonProps, type PolicyContext, type PolicyFunction, ProtectedRoute, type ProtectedRouteProps, SessionMonitor, type SessionMonitorProps, type SilentRenewConfig, type StoredOidcSessionArgs, type StoredOidcSessionInfo, type StoredOidcSessionUser, TokenBridge, type TokenBridgeProps, TokenBridgeSync, TokenLifecycleManager, type TokenLifecycleManagerProps, type TokenListener, type TokenResponse, type UseProtectedFetchOptions, type UseProtectedFetchReturn, type UseRolesReturn, type UseScopesReturn, type UseSignalRConnectionOptions, type UseSignalRConnectionReturn, type UseStoredOidcSessionReturn, type UseTokenLifecycleOptions, type UseTokenLifecycleReturn, UserProfile, type UserProfileProps, createTokenBridge, defaultConfig, defaultTokenBridge, fetchUserInfo, getConfigFromEnv, getConfigPreset, getOidcUserStorageKey, getStoredOidcSession, getStoredOidcUser, hasStoredOidcRefreshToken, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useKeycloakStatus, useProtectedFetch, useRoles, useScopes, useSignalRConnection, useStoredOidcSession, useTokenLifecycle };
|
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();
|
|
@@ -1995,6 +2032,59 @@ function useProtectedFetch(options = {}) {
|
|
|
1995
2032
|
};
|
|
1996
2033
|
}
|
|
1997
2034
|
|
|
2035
|
+
// src/utils/storedOidcSession.ts
|
|
2036
|
+
var getOidcUserStorageKey = (authority, clientId) => {
|
|
2037
|
+
if (!authority || !clientId) {
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
return `oidc.user:${authority}:${clientId}`;
|
|
2041
|
+
};
|
|
2042
|
+
var getStoredOidcUser = ({
|
|
2043
|
+
authority,
|
|
2044
|
+
clientId,
|
|
2045
|
+
storage
|
|
2046
|
+
}) => {
|
|
2047
|
+
const storageKey = getOidcUserStorageKey(authority, clientId);
|
|
2048
|
+
if (!storageKey || !storage) {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
const storedUser = storage.getItem(storageKey);
|
|
2053
|
+
if (!storedUser) {
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
return JSON.parse(storedUser);
|
|
2057
|
+
} catch {
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
var hasStoredOidcRefreshToken = (args) => {
|
|
2062
|
+
const storedUser = getStoredOidcUser(args);
|
|
2063
|
+
return typeof storedUser?.refresh_token === "string" && storedUser.refresh_token.length > 0;
|
|
2064
|
+
};
|
|
2065
|
+
var getStoredOidcSession = (args) => {
|
|
2066
|
+
const storageKey = getOidcUserStorageKey(args.authority, args.clientId);
|
|
2067
|
+
const storedUser = getStoredOidcUser(args);
|
|
2068
|
+
return {
|
|
2069
|
+
storageKey,
|
|
2070
|
+
storedUser,
|
|
2071
|
+
hasStoredUser: storedUser !== null,
|
|
2072
|
+
hasRefreshToken: typeof storedUser?.refresh_token === "string" && storedUser.refresh_token.length > 0
|
|
2073
|
+
};
|
|
2074
|
+
};
|
|
2075
|
+
|
|
2076
|
+
// src/hooks/useStoredOidcSession.ts
|
|
2077
|
+
var useStoredOidcSession = () => {
|
|
2078
|
+
const auth = useAuth();
|
|
2079
|
+
const { storage } = useStorageConfig();
|
|
2080
|
+
const authority = auth.settings.authority;
|
|
2081
|
+
const clientId = auth.settings.client_id;
|
|
2082
|
+
return react.useMemo(
|
|
2083
|
+
() => getStoredOidcSession({ authority, clientId, storage }),
|
|
2084
|
+
[authority, clientId, storage]
|
|
2085
|
+
);
|
|
2086
|
+
};
|
|
2087
|
+
|
|
1998
2088
|
// node_modules/@microsoft/signalr/dist/esm/Errors.js
|
|
1999
2089
|
var HttpError = class extends Error {
|
|
2000
2090
|
/** Constructs a new instance of {@link @microsoft/signalr.HttpError}.
|
|
@@ -5052,6 +5142,10 @@ exports.defaultTokenBridge = defaultTokenBridge;
|
|
|
5052
5142
|
exports.fetchUserInfo = fetchUserInfo;
|
|
5053
5143
|
exports.getConfigFromEnv = getConfigFromEnv;
|
|
5054
5144
|
exports.getConfigPreset = getConfigPreset;
|
|
5145
|
+
exports.getOidcUserStorageKey = getOidcUserStorageKey;
|
|
5146
|
+
exports.getStoredOidcSession = getStoredOidcSession;
|
|
5147
|
+
exports.getStoredOidcUser = getStoredOidcUser;
|
|
5148
|
+
exports.hasStoredOidcRefreshToken = hasStoredOidcRefreshToken;
|
|
5055
5149
|
exports.mergeWithPreset = mergeWithPreset;
|
|
5056
5150
|
exports.pollForToken = pollForToken;
|
|
5057
5151
|
exports.requestDeviceCode = requestDeviceCode;
|
|
@@ -5061,6 +5155,7 @@ exports.useProtectedFetch = useProtectedFetch;
|
|
|
5061
5155
|
exports.useRoles = useRoles;
|
|
5062
5156
|
exports.useScopes = useScopes;
|
|
5063
5157
|
exports.useSignalRConnection = useSignalRConnection;
|
|
5158
|
+
exports.useStoredOidcSession = useStoredOidcSession;
|
|
5064
5159
|
exports.useTokenLifecycle = useTokenLifecycle;
|
|
5065
5160
|
//# sourceMappingURL=index.js.map
|
|
5066
5161
|
//# sourceMappingURL=index.js.map
|