antaeus.keycloak.react 2.1.3 → 2.2.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 +210 -2
- package/dist/index.d.mts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ interface KeycloakConfig {
|
|
|
187
187
|
authority: string; // Required
|
|
188
188
|
clientId: string; // Required
|
|
189
189
|
scope?: string; // Default: 'openid profile email offline_access'
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
features?: {
|
|
192
192
|
autoRefresh?: boolean; // Default: true
|
|
193
193
|
sessionMonitor?: boolean; // Default: true
|
|
@@ -201,6 +201,13 @@ interface KeycloakConfig {
|
|
|
201
201
|
showLoadingIndicator?: boolean; // Default: false
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
+
advanced?: {
|
|
205
|
+
storageType?: 'session' | 'local'; // Default: 'session'
|
|
206
|
+
responseType?: string; // Default: 'code'
|
|
207
|
+
codeChallengeMethod?: string; // Default: 'S256'
|
|
208
|
+
loadUserInfo?: boolean; // Default: true
|
|
209
|
+
};
|
|
210
|
+
|
|
204
211
|
events?: {
|
|
205
212
|
onLogin?: (user: User) => void;
|
|
206
213
|
onLogout?: () => void;
|
|
@@ -210,6 +217,35 @@ interface KeycloakConfig {
|
|
|
210
217
|
}
|
|
211
218
|
```
|
|
212
219
|
|
|
220
|
+
### Storage Options
|
|
221
|
+
|
|
222
|
+
By default, the library uses **sessionStorage** which means tokens are cleared when the browser tab is closed. To persist sessions across browser tabs and restarts, use **localStorage**:
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<KeycloakProvider
|
|
226
|
+
authority="https://sso.example.com/realms/myrealm"
|
|
227
|
+
clientId="my-app"
|
|
228
|
+
config={{
|
|
229
|
+
advanced: {
|
|
230
|
+
storageType: 'local' // Persist across tabs/restarts
|
|
231
|
+
}
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<App />
|
|
235
|
+
</KeycloakProvider>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Storage Comparison:**
|
|
239
|
+
|
|
240
|
+
| Storage Type | Persistence | Use Case |
|
|
241
|
+
|--------------|-------------|----------|
|
|
242
|
+
| `session` (default) | Cleared when tab closes | Higher security, temporary sessions |
|
|
243
|
+
| `local` | Persists across tabs/restarts | Better UX, permanent sessions |
|
|
244
|
+
|
|
245
|
+
**Security Considerations:**
|
|
246
|
+
- `sessionStorage`: More secure as tokens are cleared when tab closes
|
|
247
|
+
- `localStorage`: More convenient but tokens persist longer (use `maxRefreshTokenLifetime` to limit)
|
|
248
|
+
|
|
213
249
|
## What's New in v2.0.0
|
|
214
250
|
|
|
215
251
|
- Zero-config setup
|
|
@@ -219,10 +255,144 @@ interface KeycloakConfig {
|
|
|
219
255
|
- Auto-included SessionMonitor
|
|
220
256
|
- Built-in error notifications
|
|
221
257
|
|
|
258
|
+
## Token Refresh Behavior & Edge Cases
|
|
259
|
+
|
|
260
|
+
### Understanding 401 vs 403 Errors
|
|
261
|
+
|
|
262
|
+
The library handles authentication errors intelligently:
|
|
263
|
+
|
|
264
|
+
**401 Unauthorized** - Token expired or invalid
|
|
265
|
+
- Automatically triggers silent token refresh
|
|
266
|
+
- Retries the failed request once with new token
|
|
267
|
+
- Protected by race condition prevention (1-second cooldown)
|
|
268
|
+
- Only retries when `retryOn401: true` (default)
|
|
269
|
+
|
|
270
|
+
**403 Forbidden** - Valid token, insufficient permissions
|
|
271
|
+
- Does NOT trigger token refresh (by design)
|
|
272
|
+
- Returns the 403 error to your application
|
|
273
|
+
- Indicates a permissions issue, not an auth issue
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
// Example: API call with 401 handling
|
|
277
|
+
const { fetchJson } = useProtectedFetch({
|
|
278
|
+
baseUrl: 'https://api.example.com',
|
|
279
|
+
retryOn401: true, // Default: automatically retry after refresh
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const data = await fetchJson('/api/data');
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// 401: Already handled automatically
|
|
286
|
+
// 403: User lacks permissions - handle in your app
|
|
287
|
+
// Other errors: Network, server errors, etc.
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Race Condition Protection
|
|
292
|
+
|
|
293
|
+
Multiple concurrent API calls won't cause multiple token refresh attempts:
|
|
294
|
+
|
|
295
|
+
- First 401 triggers refresh, subsequent calls wait
|
|
296
|
+
- 1-second cooldown prevents rapid retry loops
|
|
297
|
+
- Refresh state shared across all API calls
|
|
298
|
+
- Fresh token always used after refresh completes
|
|
299
|
+
|
|
300
|
+
### Refresh Token Expiration
|
|
301
|
+
|
|
302
|
+
If your refresh token expires (exceeds `maxRefreshTokenLifetime`):
|
|
303
|
+
|
|
304
|
+
1. Library detects expiration (checked every 5 minutes)
|
|
305
|
+
2. User sees "Session Expired" screen
|
|
306
|
+
3. Local storage is cleaned up
|
|
307
|
+
4. User must sign in again
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// Configure max lifetime (default: 30 days)
|
|
311
|
+
<KeycloakProvider
|
|
312
|
+
authority="..."
|
|
313
|
+
clientId="my-app"
|
|
314
|
+
config={{
|
|
315
|
+
silentRenew: {
|
|
316
|
+
maxRefreshTokenLifetime: 604800, // 7 days
|
|
317
|
+
},
|
|
318
|
+
events: {
|
|
319
|
+
onRefreshTokenExpired: () => {
|
|
320
|
+
console.log('Session exceeded max lifetime');
|
|
321
|
+
// Optional: show custom message
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Startup Token Refresh
|
|
329
|
+
|
|
330
|
+
When your app loads with an existing session:
|
|
331
|
+
|
|
332
|
+
- **Strategy: 'startup'** - Checks token immediately, refreshes if needed
|
|
333
|
+
- **Strategy: 'on-demand'** - Waits for first API call to fail
|
|
334
|
+
- **Strategy: 'both'** (recommended) - Proactive startup check + 401 retry
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
// Recommended for best UX
|
|
338
|
+
config={{
|
|
339
|
+
silentRenew: {
|
|
340
|
+
refreshStrategy: 'both',
|
|
341
|
+
thresholdSeconds: 300, // Refresh when <5min remaining
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Edge Case: Refresh Fails During Startup
|
|
347
|
+
|
|
348
|
+
If token refresh fails when the app loads:
|
|
349
|
+
|
|
350
|
+
- `onRefreshError` callback is triggered
|
|
351
|
+
- Error is logged (if debug enabled)
|
|
352
|
+
- App continues to render (non-blocking)
|
|
353
|
+
- User can manually sign in
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
<KeycloakProvider
|
|
357
|
+
authority="..."
|
|
358
|
+
clientId="my-app"
|
|
359
|
+
debug={true} // Enable console logging
|
|
360
|
+
config={{
|
|
361
|
+
events: {
|
|
362
|
+
onSilentRenewError: (error) => {
|
|
363
|
+
console.error('Token refresh failed:', error);
|
|
364
|
+
// Optional: show notification to user
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Preventing Infinite Retry Loops
|
|
372
|
+
|
|
373
|
+
The library has multiple safeguards:
|
|
374
|
+
|
|
375
|
+
1. Only retries each failed request **once**
|
|
376
|
+
2. Uses `refreshInProgressRef` flag to prevent concurrent refreshes
|
|
377
|
+
3. 1-second cooldown between refresh attempts
|
|
378
|
+
4. Refresh token expiration check prevents dead-end retries
|
|
379
|
+
|
|
380
|
+
### Network Offline Scenarios
|
|
381
|
+
|
|
382
|
+
When network is unavailable:
|
|
383
|
+
|
|
384
|
+
- Token refresh will fail (network error)
|
|
385
|
+
- Original API request fails
|
|
386
|
+
- `onRefreshError` / `onSilentRenewError` callbacks triggered
|
|
387
|
+
- No infinite retry loop (protected by cooldown)
|
|
388
|
+
- User can retry manually when online
|
|
389
|
+
|
|
222
390
|
## Troubleshooting
|
|
223
391
|
|
|
224
392
|
### 401 errors after app reopens?
|
|
225
393
|
|
|
394
|
+
Enable startup refresh strategy:
|
|
395
|
+
|
|
226
396
|
```tsx
|
|
227
397
|
config={{ silentRenew: { refreshStrategy: 'both' } }}
|
|
228
398
|
```
|
|
@@ -235,7 +405,45 @@ Make sure offline_access scope is included:
|
|
|
235
405
|
config={{ scope: 'openid profile email offline_access' }}
|
|
236
406
|
```
|
|
237
407
|
|
|
408
|
+
### Getting 403 errors on API calls?
|
|
409
|
+
|
|
410
|
+
This is a permissions issue, not authentication:
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
// 403 means valid token but insufficient permissions
|
|
414
|
+
// Check user roles/permissions in Keycloak
|
|
415
|
+
const { hasRole } = useRoles();
|
|
416
|
+
|
|
417
|
+
if (!hasRole('admin')) {
|
|
418
|
+
return <div>Access denied</div>;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Token refreshing too frequently?
|
|
423
|
+
|
|
424
|
+
Increase the threshold:
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
config={{
|
|
428
|
+
silentRenew: {
|
|
429
|
+
thresholdSeconds: 600, // Refresh when <10min remaining (default: 5min)
|
|
430
|
+
}
|
|
431
|
+
}}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Want to disable automatic retry on 401?
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
const { fetchJson } = useProtectedFetch({
|
|
438
|
+
baseUrl: 'https://api.example.com',
|
|
439
|
+
retryOn401: false, // Disable automatic retry
|
|
440
|
+
onUnauthorized: () => {
|
|
441
|
+
console.log('Got 401, handle manually');
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
238
446
|
## License
|
|
239
447
|
|
|
240
|
-
MIT � Saeed Aghdam
|
|
448
|
+
MIT � Saeed Aghdam
|
|
241
449
|
|
package/dist/index.d.mts
CHANGED
|
@@ -163,6 +163,13 @@ interface AdvancedOidcOptions {
|
|
|
163
163
|
* @default false
|
|
164
164
|
*/
|
|
165
165
|
automaticSilentSignIn?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Storage mechanism for tokens and user data
|
|
168
|
+
* - 'session': Use sessionStorage (cleared when tab closes)
|
|
169
|
+
* - 'local': Use localStorage (persists across browser sessions)
|
|
170
|
+
* @default 'session'
|
|
171
|
+
*/
|
|
172
|
+
storageType?: 'session' | 'local';
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
/**
|
|
@@ -319,6 +326,21 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
319
326
|
* </KeycloakProvider>
|
|
320
327
|
* ```
|
|
321
328
|
*
|
|
329
|
+
* @example Using localStorage to persist sessions across tabs/windows
|
|
330
|
+
* ```tsx
|
|
331
|
+
* <KeycloakProvider
|
|
332
|
+
* authority="https://sso.example.com/realms/myrealm"
|
|
333
|
+
* clientId="my-app"
|
|
334
|
+
* config={{
|
|
335
|
+
* advanced: {
|
|
336
|
+
* storageType: 'local', // Use localStorage instead of sessionStorage
|
|
337
|
+
* }
|
|
338
|
+
* }}
|
|
339
|
+
* >
|
|
340
|
+
* <YourApp />
|
|
341
|
+
* </KeycloakProvider>
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
322
344
|
* ## Full-Config Mode (Advanced)
|
|
323
345
|
*
|
|
324
346
|
* Complete control over all configuration options.
|
package/dist/index.d.ts
CHANGED
|
@@ -163,6 +163,13 @@ interface AdvancedOidcOptions {
|
|
|
163
163
|
* @default false
|
|
164
164
|
*/
|
|
165
165
|
automaticSilentSignIn?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Storage mechanism for tokens and user data
|
|
168
|
+
* - 'session': Use sessionStorage (cleared when tab closes)
|
|
169
|
+
* - 'local': Use localStorage (persists across browser sessions)
|
|
170
|
+
* @default 'session'
|
|
171
|
+
*/
|
|
172
|
+
storageType?: 'session' | 'local';
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
/**
|
|
@@ -319,6 +326,21 @@ type KeycloakProviderProps = KeycloakProviderZeroConfigProps | KeycloakProviderF
|
|
|
319
326
|
* </KeycloakProvider>
|
|
320
327
|
* ```
|
|
321
328
|
*
|
|
329
|
+
* @example Using localStorage to persist sessions across tabs/windows
|
|
330
|
+
* ```tsx
|
|
331
|
+
* <KeycloakProvider
|
|
332
|
+
* authority="https://sso.example.com/realms/myrealm"
|
|
333
|
+
* clientId="my-app"
|
|
334
|
+
* config={{
|
|
335
|
+
* advanced: {
|
|
336
|
+
* storageType: 'local', // Use localStorage instead of sessionStorage
|
|
337
|
+
* }
|
|
338
|
+
* }}
|
|
339
|
+
* >
|
|
340
|
+
* <YourApp />
|
|
341
|
+
* </KeycloakProvider>
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
322
344
|
* ## Full-Config Mode (Advanced)
|
|
323
345
|
*
|
|
324
346
|
* Complete control over all configuration options.
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var reactOidcContext = require('react-oidc-context');
|
|
5
|
+
var oidcClientTs = require('oidc-client-ts');
|
|
5
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
7
|
var reactRouterDom = require('react-router-dom');
|
|
7
8
|
var qrcode_react = require('qrcode.react');
|
|
8
|
-
var oidcClientTs = require('oidc-client-ts');
|
|
9
9
|
|
|
10
10
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
11
11
|
// src/components/KeycloakProvider.tsx
|
|
@@ -36,7 +36,8 @@ var defaultConfig = {
|
|
|
36
36
|
responseType: "code",
|
|
37
37
|
codeChallengeMethod: "S256",
|
|
38
38
|
loadUserInfo: true,
|
|
39
|
-
automaticSilentSignIn: false
|
|
39
|
+
automaticSilentSignIn: false,
|
|
40
|
+
storageType: "session"
|
|
40
41
|
},
|
|
41
42
|
events: {}
|
|
42
43
|
};
|
|
@@ -53,6 +54,8 @@ var KeycloakConfigBuilder = class {
|
|
|
53
54
|
static build(userConfig) {
|
|
54
55
|
this.validate(userConfig);
|
|
55
56
|
const config = this.mergeWithDefaults(userConfig);
|
|
57
|
+
const storageType = config.advanced.storageType || "session";
|
|
58
|
+
const storage = storageType === "local" ? window.localStorage : window.sessionStorage;
|
|
56
59
|
const oidcSettings = {
|
|
57
60
|
authority: config.authority,
|
|
58
61
|
client_id: config.clientId,
|
|
@@ -63,6 +66,8 @@ var KeycloakConfigBuilder = class {
|
|
|
63
66
|
automaticSilentRenew: config.silentRenew.enabled,
|
|
64
67
|
loadUserInfo: config.advanced.loadUserInfo,
|
|
65
68
|
silent_redirect_uri: config.silentRenew.silentRedirectUri,
|
|
69
|
+
// Configure storage for user data and tokens
|
|
70
|
+
userStore: new oidcClientTs.WebStorageStateStore({ store: storage }),
|
|
66
71
|
// Add PKCE support (required for public clients)
|
|
67
72
|
...config.advanced.codeChallengeMethod && {
|
|
68
73
|
response_mode: "query"
|