@bmc-soft/keycloak-auth 1.0.11 → 2.0.2
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 +123 -60
- package/dist/_lib/types/confirmAuthPhase.d.ts +1 -2
- package/dist/_lib/types/confirmAuthPhase.d.ts.map +1 -1
- package/dist/_lib/types/confirmAuthPhase.js +1 -2
- package/dist/_lib/types/confirmAuthPhase.js.map +1 -1
- package/dist/axios/adapters/KeycloakTokenProvider.js +8 -8
- package/dist/axios/adapters/KeycloakTokenProvider.js.map +1 -1
- package/dist/axios/interceptors.js +2 -2
- package/dist/axios/interceptors.js.map +1 -1
- package/dist/context/KeycloakConfigContext.d.ts +20 -1
- package/dist/context/KeycloakConfigContext.d.ts.map +1 -1
- package/dist/context/KeycloakConfigContext.js +52 -3
- package/dist/context/KeycloakConfigContext.js.map +1 -1
- package/dist/context/KeycloakInstanceContext.d.ts +2 -0
- package/dist/context/KeycloakInstanceContext.d.ts.map +1 -1
- package/dist/context/KeycloakInstanceContext.js +93 -18
- package/dist/context/KeycloakInstanceContext.js.map +1 -1
- package/dist/context/KeycloakProvider.d.ts +8 -1
- package/dist/context/KeycloakProvider.d.ts.map +1 -1
- package/dist/context/KeycloakProvider.js +3 -3
- package/dist/context/KeycloakProvider.js.map +1 -1
- package/dist/context/ReauthContext.d.ts +3 -1
- package/dist/context/ReauthContext.d.ts.map +1 -1
- package/dist/context/ReauthContext.js +6 -2
- package/dist/context/ReauthContext.js.map +1 -1
- package/dist/context/TokenContext.d.ts +2 -0
- package/dist/context/TokenContext.d.ts.map +1 -1
- package/dist/context/TokenContext.js +32 -10
- package/dist/context/TokenContext.js.map +1 -1
- package/dist/context/index.d.ts +2 -2
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/reauthRequiredRef.d.ts +2 -1
- package/dist/context/reauthRequiredRef.d.ts.map +1 -1
- package/dist/context/reauthRequiredRef.js.map +1 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +37 -4
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/sessionPolicy.d.ts +21 -0
- package/dist/core/sessionPolicy.d.ts.map +1 -0
- package/dist/core/sessionPolicy.js +63 -0
- package/dist/core/sessionPolicy.js.map +1 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/hooks/useKeycloakAuth.d.ts +3 -0
- package/dist/hooks/useKeycloakAuth.d.ts.map +1 -1
- package/dist/hooks/useKeycloakAuth.js +12 -7
- package/dist/hooks/useKeycloakAuth.js.map +1 -1
- package/dist/hooks/useKeycloakAuthScreen.d.ts +1 -1
- package/dist/hooks/useKeycloakAuthScreen.d.ts.map +1 -1
- package/dist/hooks/useKeycloakAuthScreen.js +3 -2
- package/dist/hooks/useKeycloakAuthScreen.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/screens/AuthPage/AuthPage.d.ts +2 -0
- package/dist/screens/AuthPage/AuthPage.d.ts.map +1 -1
- package/dist/screens/AuthPage/AuthPage.js +12 -15
- package/dist/screens/AuthPage/AuthPage.js.map +1 -1
- package/dist/screens/ConfirmAuthPage/ConfirmAuthPage.d.ts +3 -1
- package/dist/screens/ConfirmAuthPage/ConfirmAuthPage.d.ts.map +1 -1
- package/dist/screens/ConfirmAuthPage/ConfirmAuthPage.js +132 -57
- package/dist/screens/ConfirmAuthPage/ConfirmAuthPage.js.map +1 -1
- package/dist/screens/ConfirmAuthPage/index.d.ts +1 -0
- package/dist/screens/ConfirmAuthPage/index.d.ts.map +1 -1
- package/dist/screens/index.d.ts +1 -0
- package/dist/screens/index.d.ts.map +1 -1
- package/dist/storage/credentialStorage.d.ts +8 -0
- package/dist/storage/credentialStorage.d.ts.map +1 -1
- package/dist/storage/credentialStorage.js +99 -1
- package/dist/storage/credentialStorage.js.map +1 -1
- package/dist/storage/tokenStorage.d.ts +10 -0
- package/dist/storage/tokenStorage.d.ts.map +1 -1
- package/dist/storage/tokenStorage.js +30 -8
- package/dist/storage/tokenStorage.js.map +1 -1
- package/dist/ui/LogoutConfirmSheet/LogoutConfirmSheet.d.ts.map +1 -1
- package/dist/ui/LogoutConfirmSheet/LogoutConfirmSheet.js +10 -7
- package/dist/ui/LogoutConfirmSheet/LogoutConfirmSheet.js.map +1 -1
- package/dist/ui/NumberPad/NumberPad.d.ts.map +1 -1
- package/dist/ui/NumberPad/NumberPad.js +16 -9
- package/dist/ui/NumberPad/NumberPad.js.map +1 -1
- package/dist/ui/PINConfirm/PINConfirm.d.ts +1 -0
- package/dist/ui/PINConfirm/PINConfirm.d.ts.map +1 -1
- package/dist/ui/PINConfirm/PINConfirm.js +40 -12
- package/dist/ui/PINConfirm/PINConfirm.js.map +1 -1
- package/dist/ui/PINSetup/stages/BiometryStage.d.ts.map +1 -1
- package/dist/ui/PINSetup/stages/BiometryStage.js +6 -2
- package/dist/ui/PINSetup/stages/BiometryStage.js.map +1 -1
- package/dist/ui/PINSetup/styles.d.ts +0 -1
- package/dist/ui/PINSetup/styles.d.ts.map +1 -1
- package/dist/ui/PINSetup/styles.js +0 -1
- package/dist/ui/PINSetup/styles.js.map +1 -1
- package/dist/ui/WebViewLogin/WebViewLogin.d.ts.map +1 -1
- package/dist/ui/WebViewLogin/WebViewLogin.js +2 -2
- package/dist/ui/WebViewLogin/WebViewLogin.js.map +1 -1
- package/dist/widgets/ReauthBottomSheet/ReauthBottomSheet.d.ts +2 -0
- package/dist/widgets/ReauthBottomSheet/ReauthBottomSheet.d.ts.map +1 -1
- package/dist/widgets/ReauthBottomSheet/ReauthBottomSheet.js +19 -8
- package/dist/widgets/ReauthBottomSheet/ReauthBottomSheet.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -60,9 +60,9 @@ import { AuthPage } from '@bmc-soft/keycloak-auth';
|
|
|
60
60
|
const LoginScreen = () => (
|
|
61
61
|
<AuthPage
|
|
62
62
|
logo={require('./logo.png')}
|
|
63
|
-
onSuccess={(
|
|
64
|
-
// Сохраняем
|
|
65
|
-
|
|
63
|
+
onSuccess={(accessToken) => {
|
|
64
|
+
// Сохраняем access token в сессию приложения и переходим
|
|
65
|
+
Session.events.onLogin(accessToken);
|
|
66
66
|
navigation.replace('Home');
|
|
67
67
|
}}
|
|
68
68
|
onError={(err) => console.error(err)}
|
|
@@ -74,6 +74,18 @@ const LoginScreen = () => (
|
|
|
74
74
|
|
|
75
75
|
Если используете axios, задайте провайдер токенов (см. [Axios](#axios)) и вызовите `setupAxiosInterceptors` с `tokenProvider` и `onReauthRequired`. Провайдер обычно настраивается внутри KeycloakProvider после инициализации keycloak (см. [Интеграция](#интеграция)).
|
|
76
76
|
|
|
77
|
+
### Требования к Keycloak client
|
|
78
|
+
|
|
79
|
+
Для нового mobile flow настройте Keycloak client как:
|
|
80
|
+
|
|
81
|
+
- **Client type**: public
|
|
82
|
+
- **Flow**: Authorization Code / Standard Flow
|
|
83
|
+
- **PKCE**: required, `S256`
|
|
84
|
+
- **Scope**: разрешён `offline_access`
|
|
85
|
+
- **Redirect URI**: только URI приложения, например `myapp://callback`
|
|
86
|
+
|
|
87
|
+
`client_secret` не передаётся и не хранится в React Native приложении.
|
|
88
|
+
|
|
77
89
|
---
|
|
78
90
|
|
|
79
91
|
## Провайдер и конфигурация
|
|
@@ -83,14 +95,22 @@ const LoginScreen = () => (
|
|
|
83
95
|
| Проп | Тип | Обязательный | Описание |
|
|
84
96
|
|------|-----|--------------|----------|
|
|
85
97
|
| `children` | ReactNode | да | Дерево приложения |
|
|
86
|
-
| `config` | KeycloakConfigWith2FA | да | `url`, `realm`, `clientId`;
|
|
98
|
+
| `config` | KeycloakConfigWith2FA | да | `url`, `realm`, `clientId`; `clientId2fa` оставлен для legacy-сценариев, новый мобильный flow использует один public client + PKCE |
|
|
87
99
|
| `redirectUri` | string | да | URI OAuth callback (например `myapp://callback`) |
|
|
88
100
|
| `theme` | KeycloakTheme | нет | Цвета, шрифты, LoaderComponent, компоненты кнопок |
|
|
89
101
|
| `onTokens` | (tokens: KeycloakTokens) => void | нет | Вызывается при изменении токенов (логин, refresh, logout) |
|
|
102
|
+
| `offlineAccessEnabled` | boolean | нет | Запрашивать `offline_access` в login URL; по умолчанию `true` |
|
|
103
|
+
| `backgroundReauth` | `{ enabled?: boolean; thresholdMs?: number; internalNetworkMode?: 'ip-host-is-internal' }` | нет | Настройки PIN/биометрии после возврата из background; по умолчанию `enabled: true`, `thresholdMs: 60000` |
|
|
104
|
+
| `clientSecret` | string | нет | Секрет confidential client для legacy-конфигураций; для новых мобильных flows предпочтителен public client + PKCE |
|
|
105
|
+
| `getClientSecret` | () => string \| undefined \| Promise<string \| undefined> | нет | Ленивый резолвер секрета; имеет приоритет над `clientSecret` |
|
|
106
|
+
| `initRecoveryStrategy` | `'none' \| 'clear-tokens-and-retry'` | нет | Что делать, если init со stored tokens упал; по умолчанию `none` |
|
|
107
|
+
| `deferStoredTokensUntilRefresh` | boolean | нет | Не передавать persisted tokens в `keycloak.init()` до локального PIN/биометрии; по умолчанию `false` |
|
|
90
108
|
| `autoRefreshToken` | boolean | нет | По умолчанию `true` |
|
|
91
109
|
| `autoRefreshTokenMinValidity` | number | нет | За сколько секунд до истечения вызывать refresh; по умолчанию `5` |
|
|
92
110
|
| `onReauthRequired` | () => void | нет | Вызывается при ошибке обновления токена (например показать экран реавторизации) |
|
|
93
111
|
|
|
112
|
+
`deferStoredTokensUntilRefresh` полезен для mobile unlock flow: приложение может сначала показать PIN/биометрию, а уже после успешного локального подтверждения выполнить refresh через сохранённый offline token. Это предотвращает преждевременный расход refresh/offline token при старте приложения.
|
|
113
|
+
|
|
94
114
|
### KeycloakTheme
|
|
95
115
|
|
|
96
116
|
Все поля опциональны; недостающие подставляются из дефолтной темы. Передаётся в KeycloakProvider как `theme`.
|
|
@@ -124,20 +144,27 @@ const LoginScreen = () => (
|
|
|
124
144
|
|
|
125
145
|
#### ConfirmAuthPage
|
|
126
146
|
|
|
127
|
-
Подтверждение сессии:
|
|
147
|
+
Подтверждение сессии: сначала PIN/биометрия, затем продолжение с текущим access token или refresh через offline token. Если repeat-login не может восстановить токены через offline token, открывается обычный Keycloak login. Скрытый ввод username/password в новом flow не используется.
|
|
148
|
+
|
|
149
|
+
Режимы:
|
|
150
|
+
|
|
151
|
+
- `login` — повторный вход в приложение; при невозможности refresh открывает обычный Keycloak login.
|
|
152
|
+
- `unlock` — локальная разблокировка после background; refresh выполняется только если access token уже истёк.
|
|
153
|
+
- `reauth` — повторная авторизация после 401; refresh через offline token обязателен.
|
|
128
154
|
|
|
129
155
|
| Проп | Тип | По умолчанию | Описание |
|
|
130
156
|
|------|-----|--------------|----------|
|
|
131
|
-
| `onSuccess` | () => void | — |
|
|
157
|
+
| `onSuccess` | () => void | — | Успешное локальное подтверждение и восстановление/проверка access token |
|
|
132
158
|
| `onError` | (error: Error) => void | — | Например неверный PIN |
|
|
133
159
|
| `onLogout` | () => void | — | После очистки хранилища пакета; в приложении — очистить сессию и перейти на экран входа |
|
|
160
|
+
| `mode` | `'login' \| 'unlock' \| 'reauth'` | `"login"` | Сценарий: повторный вход, unlock после background или reauth после 401 |
|
|
134
161
|
| `logoutText` | string | "Выйти из аккаунта" | Текст ссылки «Выйти» |
|
|
135
162
|
| `logo`, `logoHeight`, `logoWidth` | — | 80 | Логотип |
|
|
136
163
|
| `pinLength` | number | 4 | Длина PIN |
|
|
137
164
|
| `allowBiometry`, `autoShowBiometry` | boolean | true | Биометрия |
|
|
138
165
|
| `title`, `description` | string | — | Заголовок и описание блока PIN |
|
|
166
|
+
| `layout` | `'screen' \| 'sheet'` | `"screen"` | Компактная раскладка `sheet` для встраивания в BottomSheet |
|
|
139
167
|
| `style` | ViewStyle | — | Стиль контейнера |
|
|
140
|
-
| `webViewTimeoutMs` | number | 3000 | Задержка (мс) в фазе webview_detect перед переходом на PIN |
|
|
141
168
|
|
|
142
169
|
### Виджеты
|
|
143
170
|
|
|
@@ -152,10 +179,9 @@ BottomSheet с подтверждением PIN для реавторизаци
|
|
|
152
179
|
| `onError` | (error: Error) => void | — | Ошибка ввода PIN |
|
|
153
180
|
| `pinLength` | number | 4 | Длина PIN |
|
|
154
181
|
| `allowBiometry`, `autoShowBiometry` | boolean | true | Биометрия |
|
|
155
|
-
| `
|
|
182
|
+
| `mode` | `'login' \| 'unlock' \| 'reauth'` | авто | Если не передан, `background` открывается как `unlock`, остальные причины как `reauth` |
|
|
183
|
+
| `title` | string | "Введите PIN" | Заголовок |
|
|
156
184
|
| `description` | string | "Введите PIN-код" | Описание |
|
|
157
|
-
| `footer` | ReactNode | — | Опциональный футер |
|
|
158
|
-
| `snapPointPercentage` | number | 50 | Высота sheet в % |
|
|
159
185
|
|
|
160
186
|
### Кнопки выхода
|
|
161
187
|
|
|
@@ -187,15 +213,15 @@ BottomSheet с подтверждением PIN для реавторизаци
|
|
|
187
213
|
|
|
188
214
|
Полный API авторизации: инстанс keycloak, токен, login, logout, URL, состояние реавторизации.
|
|
189
215
|
|
|
190
|
-
Возвращает: `keycloak`, `isInitialized`, `isLoading`, `error`, `token`, `isExpired`, `isAuthenticated`, `updateToken`, `login`, `logout`, `loadUserProfile`, `createLoginUrl`, `createLogoutUrl`, `clearTokens`, `isReauthRequired`, `showReauth`, `hideReauth`.
|
|
216
|
+
Возвращает: `keycloak`, `isInitialized`, `isLoading`, `error`, `accessToken`, `token`, `offlineToken`, `refreshToken`, `isExpired`, `isAuthenticated`, `updateToken`, `login`, `logout`, `loadUserProfile`, `createLoginUrl`, `createLogoutUrl`, `clearTokens`, `isReauthRequired`, `showReauth`, `hideReauth`.
|
|
191
217
|
|
|
192
218
|
### useToken()
|
|
193
219
|
|
|
194
|
-
Доступ только к токенам (меньше ре-рендеров). Возвращает: `token`, `refreshToken`, `idToken`, `isExpired`, `updateToken`, `clearTokens`.
|
|
220
|
+
Доступ только к токенам (меньше ре-рендеров). Возвращает: `accessToken`, `token`, `offlineToken`, `refreshToken`, `idToken`, `isExpired`, `updateToken`, `clearTokens`.
|
|
195
221
|
|
|
196
222
|
### useReauth()
|
|
197
223
|
|
|
198
|
-
Состояние UI реавторизации. Возвращает: `isReauthRequired`, `showReauth`, `hideReauth`.
|
|
224
|
+
Состояние UI реавторизации. Возвращает: `isReauthRequired`, `reauthReason`, `showReauth`, `hideReauth`.
|
|
199
225
|
|
|
200
226
|
**Состояние isReauthRequired:**
|
|
201
227
|
|
|
@@ -209,7 +235,7 @@ BottomSheet с подтверждением PIN для реавторизаци
|
|
|
209
235
|
|
|
210
236
|
Возвращает: `screen: 'login' | 'confirm' | null`, `isLoading`, `error`.
|
|
211
237
|
|
|
212
|
-
Опции: `shouldShowConfirm?: () => Promise<boolean>` — переопределить стандартную проверку (наличие
|
|
238
|
+
Опции: `shouldShowConfirm?: () => Promise<boolean>` — переопределить стандартную проверку (наличие offline token и PIN).
|
|
213
239
|
|
|
214
240
|
### useKeycloakTheme()
|
|
215
241
|
|
|
@@ -221,15 +247,15 @@ BottomSheet с подтверждением PIN для реавторизаци
|
|
|
221
247
|
|
|
222
248
|
### tokenStorage
|
|
223
249
|
|
|
224
|
-
Хранение токенов в Keychain. API: `getToken`, `saveToken`, `getRefreshToken`, `saveRefreshToken`, `getIdToken`, `saveIdToken`, `getTokens`, `saveTokens`, `clearTokens`, `hasTokens`.
|
|
250
|
+
Хранение токенов в Keychain. API: `getToken`/`getAccessToken`, `saveToken`/`saveAccessToken`, `getRefreshToken`/`getOfflineToken`, `saveRefreshToken`/`saveOfflineToken`, `getIdToken`, `saveIdToken`, `getTokens`, `saveTokens`, `clearTokens`, `hasTokens`.
|
|
225
251
|
|
|
226
|
-
После успешного ConfirmAuthPage
|
|
252
|
+
`refresh_token` из Keycloak при `offline_access` хранится как offline token. Он не отправляется в backend; backend получает только `Authorization: Bearer <accessToken>`. После успешного ConfirmAuthPage вызовите `tokenStorage.getAccessToken()`, чтобы получить текущий access token и обновить сессию приложения.
|
|
227
253
|
|
|
228
254
|
> **Важно**: не храните токены в AsyncStorage. Используйте только Keychain через пакет.
|
|
229
255
|
|
|
230
256
|
### credentialStorage
|
|
231
257
|
|
|
232
|
-
Используется внутри пакета для
|
|
258
|
+
Используется внутри пакета для PIN-verifier и биометрии. Legacy-хранение зашифрованных credentials оставлено для совместимости, но новый offline_access flow не использует username/password для повторного входа.
|
|
233
259
|
|
|
234
260
|
---
|
|
235
261
|
|
|
@@ -264,7 +290,7 @@ BottomSheet с подтверждением PIN для реавторизаци
|
|
|
264
290
|
|
|
265
291
|
### 1. Оборачивание приложения в KeycloakProvider
|
|
266
292
|
|
|
267
|
-
Конфиг и `redirectUri` берут из настроек приложения. Тему (Loader, кнопки, цвета) передайте из темы приложения. В `onTokens` сохраняйте
|
|
293
|
+
Конфиг и `redirectUri` берут из настроек приложения. Тему (Loader, кнопки, цвета) передайте из темы приложения. В `onTokens` сохраняйте access token в хранилище сессии; в `onReauthRequired` показывайте экран реавторизации.
|
|
268
294
|
|
|
269
295
|
**Порядок провайдеров**: если используется `BottomSheetModalProvider` из `@gorhom/bottom-sheet` (например для реавторизации или других модалок), он должен находиться **внутри** KeycloakProvider. Иначе ReauthBottomSheet или ConfirmAuthPage внутри модалки не получат контекст Keycloak. Порядок: `KeycloakProvider` → `BottomSheetModalProvider` → остальное дерево приложения.
|
|
270
296
|
|
|
@@ -297,8 +323,8 @@ export const App = () => (
|
|
|
297
323
|
}}
|
|
298
324
|
redirectUri="myapp://callback"
|
|
299
325
|
theme={theme}
|
|
300
|
-
onTokens={({
|
|
301
|
-
Session.events.onChangeAuthToken(
|
|
326
|
+
onTokens={({ accessToken }) => {
|
|
327
|
+
Session.events.onChangeAuthToken(accessToken);
|
|
302
328
|
}}
|
|
303
329
|
onReauthRequired={() => {
|
|
304
330
|
Session.events.onRequireReauth();
|
|
@@ -313,26 +339,34 @@ export const App = () => (
|
|
|
313
339
|
|
|
314
340
|
### 2. Установка TokenProvider для axios
|
|
315
341
|
|
|
316
|
-
Провайдер токенов настраивается один раз внутри KeycloakProvider после инициализации keycloak
|
|
342
|
+
Провайдер токенов настраивается один раз внутри KeycloakProvider после инициализации keycloak. Он отдаёт backend только access token, а refresh выполняет через offline token.
|
|
317
343
|
|
|
318
344
|
```tsx
|
|
319
|
-
import {
|
|
320
|
-
|
|
345
|
+
import {
|
|
346
|
+
KeycloakTokenProvider,
|
|
347
|
+
setupAxiosInterceptors,
|
|
348
|
+
useKeycloakAuth,
|
|
349
|
+
} from '@bmc-soft/keycloak-auth';
|
|
350
|
+
|
|
351
|
+
const api = axios.create({ baseURL: 'https://api.example.com' });
|
|
321
352
|
|
|
322
353
|
const KeycloakAxiosTokenProviderSetup = ({ children }) => {
|
|
323
354
|
const { keycloak, isInitialized } = useKeycloakAuth();
|
|
324
355
|
|
|
325
356
|
useEffect(() => {
|
|
326
|
-
if (isInitialized
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
357
|
+
if (!isInitialized || !keycloak) return undefined;
|
|
358
|
+
|
|
359
|
+
const { cleanup } = setupAxiosInterceptors(api, {
|
|
360
|
+
tokenProvider: new KeycloakTokenProvider(keycloak),
|
|
361
|
+
onReauthRequired: () => {
|
|
362
|
+
Session.events.onRequireReauth();
|
|
363
|
+
},
|
|
364
|
+
onTokenRefreshed: (accessToken) => {
|
|
365
|
+
Session.events.onChangeAuthToken(accessToken);
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return cleanup;
|
|
336
370
|
}, [isInitialized, keycloak]);
|
|
337
371
|
|
|
338
372
|
return <>{children}</>;
|
|
@@ -348,29 +382,27 @@ const KeycloakAxiosTokenProviderSetup = ({ children }) => {
|
|
|
348
382
|
</KeycloakProvider>
|
|
349
383
|
```
|
|
350
384
|
|
|
351
|
-
|
|
385
|
+
Если у приложения уже есть свой `tokenProvider`, его методы должны соблюдать тот же контракт:
|
|
352
386
|
|
|
353
387
|
```tsx
|
|
354
|
-
import { setupAxiosInterceptors } from '@bmc-soft/keycloak-auth';
|
|
355
|
-
import { getKeycloakTokenProvider } from './keycloakTokenProvider';
|
|
356
|
-
|
|
357
|
-
const api = axios.create({ baseURL: 'https://api.example.com' });
|
|
358
|
-
|
|
359
388
|
setupAxiosInterceptors(api, {
|
|
360
389
|
tokenProvider: {
|
|
361
|
-
getToken: () =>
|
|
362
|
-
refreshToken: () =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
390
|
+
getToken: () => keycloak.token || null,
|
|
391
|
+
refreshToken: async () => {
|
|
392
|
+
await keycloak.updateToken(-1);
|
|
393
|
+
return keycloak.token || null;
|
|
394
|
+
},
|
|
395
|
+
hasRefreshToken: () => Boolean(keycloak.refreshToken),
|
|
396
|
+
},
|
|
397
|
+
onReauthRequired: () => {
|
|
398
|
+
Session.events.onRequireReauth();
|
|
366
399
|
},
|
|
367
|
-
onReauthRequired: () => Session.events.onRequireReauth(),
|
|
368
400
|
});
|
|
369
401
|
```
|
|
370
402
|
|
|
371
403
|
### 3. Стек авторизации (Login + Confirm)
|
|
372
404
|
|
|
373
|
-
Два экрана: Login (AuthPage) и Confirm (ConfirmAuthPage). Начальный маршрут задаётся через `useKeycloakAuthScreen()`: показывать Confirm, если есть
|
|
405
|
+
Два экрана: Login (AuthPage) и Confirm (ConfirmAuthPage). Начальный маршрут задаётся через `useKeycloakAuthScreen()`: показывать Confirm, если есть offline token и PIN, иначе Login.
|
|
374
406
|
|
|
375
407
|
```tsx
|
|
376
408
|
import { useKeycloakAuthScreen, AuthPage, ConfirmAuthPage, tokenStorage } from '@bmc-soft/keycloak-auth';
|
|
@@ -395,7 +427,7 @@ export const AuthStack = () => {
|
|
|
395
427
|
const LoginScreen = () => (
|
|
396
428
|
<AuthPage
|
|
397
429
|
logo={require('./logo.png')}
|
|
398
|
-
onSuccess={(
|
|
430
|
+
onSuccess={(accessToken) => Session.events.onLogin(accessToken)}
|
|
399
431
|
onError={(err) => console.error(err)}
|
|
400
432
|
/>
|
|
401
433
|
);
|
|
@@ -404,8 +436,8 @@ const ConfirmScreen = () => {
|
|
|
404
436
|
const navigation = useNavigation();
|
|
405
437
|
|
|
406
438
|
const onSuccess = useCallback(async () => {
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
439
|
+
const accessToken = await tokenStorage.getAccessToken();
|
|
440
|
+
if (accessToken) Session.events.onLogin(accessToken);
|
|
409
441
|
}, []);
|
|
410
442
|
|
|
411
443
|
return (
|
|
@@ -423,7 +455,7 @@ const ConfirmScreen = () => {
|
|
|
423
455
|
|
|
424
456
|
### 4. Реавторизация при 401
|
|
425
457
|
|
|
426
|
-
При 401 интерцепторы вызывают `onReauthRequired`. Можно использовать `isReauthRequired` из `useKeycloakAuth()` как единственный источник правды для отображения реавторизации. Если используете готовый **ReauthBottomSheet** из пакета, он сам открывается и закрывается по `isReauthRequired` — достаточно смонтировать компонент и при необходимости передать коллбэки (`onSuccess`, `onDismiss`). Для кастомной обёртки (свой BottomSheet и контент) можно использовать **ConfirmAuthPage** и вручную управлять видимостью по `isReauthRequired`. После успешного ввода PIN получите
|
|
458
|
+
При 401 интерцепторы вызывают `onReauthRequired`. Можно использовать `isReauthRequired` из `useKeycloakAuth()` как единственный источник правды для отображения реавторизации. Если используете готовый **ReauthBottomSheet** из пакета, он сам открывается и закрывается по `isReauthRequired` — достаточно смонтировать компонент и при необходимости передать коллбэки (`onSuccess`, `onDismiss`). Для кастомной обёртки (свой BottomSheet и контент) можно использовать **ConfirmAuthPage** и вручную управлять видимостью по `isReauthRequired`. После успешного ввода PIN получите access token из пакета, обновите сессию и закройте реавторизацию. Флаг `isReauthRequired` сбрасывается пакетом автоматически при успехе в ReauthBottomSheet/ConfirmAuthPage; в `onSuccess` достаточно своей логики (обновление сессии, закрытие sheet).
|
|
427
459
|
|
|
428
460
|
Пример с кастомным BottomSheet и ConfirmAuthPage (управление видимостью вручную):
|
|
429
461
|
|
|
@@ -440,9 +472,9 @@ export const CustomReauthSheet = () => {
|
|
|
440
472
|
}, [showReauth]);
|
|
441
473
|
|
|
442
474
|
const handleSuccess = useCallback(async () => {
|
|
443
|
-
const
|
|
444
|
-
if (
|
|
445
|
-
Session.events.onLogin(
|
|
475
|
+
const accessToken = await tokenStorage.getAccessToken();
|
|
476
|
+
if (accessToken) {
|
|
477
|
+
Session.events.onLogin(accessToken);
|
|
446
478
|
Session.events.onReauthCompleted();
|
|
447
479
|
}
|
|
448
480
|
sheetRef.current?.close();
|
|
@@ -450,13 +482,35 @@ export const CustomReauthSheet = () => {
|
|
|
450
482
|
|
|
451
483
|
return (
|
|
452
484
|
<BottomSheet ref={sheetRef} snapPoints={['50%']} enablePanDownToClose>
|
|
453
|
-
<ConfirmAuthPage onSuccess={handleSuccess} pinLength={4} />
|
|
485
|
+
<ConfirmAuthPage mode="reauth" onSuccess={handleSuccess} pinLength={4} />
|
|
454
486
|
</BottomSheet>
|
|
455
487
|
);
|
|
456
488
|
};
|
|
457
489
|
```
|
|
458
490
|
|
|
459
|
-
### 5.
|
|
491
|
+
### 5. PIN/биометрия после возврата из background
|
|
492
|
+
|
|
493
|
+
Если `backgroundReauth.enabled !== false`, библиотека отслеживает `AppState`: при возврате из background/inactive через более чем `thresholdMs` миллисекунд открывается `ReauthBottomSheet`.
|
|
494
|
+
|
|
495
|
+
По умолчанию:
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
<KeycloakProvider
|
|
499
|
+
config={{ url: 'https://sso.example.com', realm: 'my-realm', clientId: 'my-app' }}
|
|
500
|
+
redirectUri="myapp://callback"
|
|
501
|
+
backgroundReauth={{
|
|
502
|
+
enabled: true,
|
|
503
|
+
thresholdMs: 60000,
|
|
504
|
+
internalNetworkMode: 'ip-host-is-internal',
|
|
505
|
+
}}
|
|
506
|
+
>
|
|
507
|
+
<App />
|
|
508
|
+
</KeycloakProvider>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Правило внутренней сети: если host в `config.url` является IP-адресом, сеть считается внутренней и background sheet не показывается; если host является DNS-именем, сеть считается внешней.
|
|
512
|
+
|
|
513
|
+
### 6. Выход
|
|
460
514
|
|
|
461
515
|
Используйте LogoutButtonIcon или LogoutButtonText. По нажатию открывается Modal с WebViewLogout; при успешном выходе вызывается `onLogoutSuccess` — там очищайте сессию приложения.
|
|
462
516
|
|
|
@@ -494,17 +548,25 @@ KeycloakProvider → инициализация Keycloak → установка
|
|
|
494
548
|
↓
|
|
495
549
|
useKeycloakAuthScreen → Login (AuthPage) или Confirm (ConfirmAuthPage)
|
|
496
550
|
↓
|
|
497
|
-
|
|
551
|
+
Confirm → PIN/биометрия → текущий access token или refresh через offline token
|
|
552
|
+
↓
|
|
553
|
+
onSuccess → Session.onLogin(accessToken) | onLogout → Session.onLogout + навигация
|
|
498
554
|
↓
|
|
499
|
-
axios 401
|
|
555
|
+
axios 401 / background > 60с вне внутренней сети → ReauthBottomSheet с ConfirmAuthPage
|
|
500
556
|
↓
|
|
501
|
-
|
|
557
|
+
PIN/биометрия → refresh при необходимости → tokenStorage.getAccessToken() → закрыть sheet
|
|
502
558
|
```
|
|
503
559
|
|
|
504
560
|
---
|
|
505
561
|
|
|
506
562
|
## Архитектура и безопасность
|
|
507
563
|
|
|
564
|
+
### Mobile client и offline_access
|
|
565
|
+
|
|
566
|
+
Для мобильного приложения используйте Keycloak public client + Authorization Code Flow with PKCE (`S256`). `client_secret` не должен попадать в React Native приложение; confidential client допустим только через backend/BFF.
|
|
567
|
+
|
|
568
|
+
При `offlineAccessEnabled=true` библиотека добавляет `offline_access` в login scope. Keycloak возвращает offline token в поле `refresh_token`; библиотека хранит его в Keychain как `offlineToken`/`refreshToken` и использует только для получения новых access token.
|
|
569
|
+
|
|
508
570
|
### Иерархия контекстов
|
|
509
571
|
|
|
510
572
|
```
|
|
@@ -520,9 +582,10 @@ KeycloakConfigProvider ← конфиг (редко меняется)
|
|
|
520
582
|
### Безопасность
|
|
521
583
|
|
|
522
584
|
- Токены в OS Keychain (не AsyncStorage)
|
|
523
|
-
- PIN
|
|
585
|
+
- PIN хранится как verifier в Keychain; legacy-хранилище encrypted credentials сохранено только для обратной совместимости
|
|
586
|
+
- Offline token не отправляется в backend и используется только для refresh в Keycloak
|
|
524
587
|
- Запросы по HTTPS
|
|
525
|
-
-
|
|
588
|
+
- Username/password не используются для repeat-login в новом offline_access flow
|
|
526
589
|
|
|
527
590
|
|
|
528
591
|
## Экспорты
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export declare const CONFIRM_AUTH_PHASE: {
|
|
2
|
-
readonly WEBVIEW_DETECT: "webview_detect";
|
|
3
2
|
readonly PIN: "pin";
|
|
4
|
-
readonly
|
|
3
|
+
readonly WEBVIEW_LOGIN: "webview_login";
|
|
5
4
|
};
|
|
6
5
|
export type ConfirmAuthPhase = (typeof CONFIRM_AUTH_PHASE)[keyof typeof CONFIRM_AUTH_PHASE];
|
|
7
6
|
//# sourceMappingURL=confirmAuthPhase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirmAuthPhase.d.ts","sourceRoot":"","sources":["../../../src/_lib/types/confirmAuthPhase.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"confirmAuthPhase.d.ts","sourceRoot":"","sources":["../../../src/_lib/types/confirmAuthPhase.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB;;;CAGrB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirmAuthPhase.js","sourceRoot":"","sources":["../../../src/_lib/types/confirmAuthPhase.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,
|
|
1
|
+
{"version":3,"file":"confirmAuthPhase.js","sourceRoot":"","sources":["../../../src/_lib/types/confirmAuthPhase.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,GAAG,EAAE,KAAK;IACV,aAAa,EAAE,eAAe;CACtB,CAAC"}
|
|
@@ -10,7 +10,7 @@ export class KeycloakTokenProvider {
|
|
|
10
10
|
return token;
|
|
11
11
|
}
|
|
12
12
|
const tokens = await tokenStorage.getTokens();
|
|
13
|
-
return tokens?.token || null;
|
|
13
|
+
return tokens?.accessToken || tokens?.token || null;
|
|
14
14
|
}
|
|
15
15
|
catch (error) {
|
|
16
16
|
console.error('[KeycloakTokenProvider] Get token error:', error);
|
|
@@ -19,15 +19,15 @@ export class KeycloakTokenProvider {
|
|
|
19
19
|
}
|
|
20
20
|
async refreshToken() {
|
|
21
21
|
try {
|
|
22
|
-
const updated = await this.keycloakClient.updateToken(
|
|
23
|
-
if (!updated) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
22
|
+
const updated = await this.keycloakClient.updateToken(-1);
|
|
26
23
|
const { token, refreshToken, idToken } = this.keycloakClient;
|
|
27
24
|
if (token) {
|
|
25
|
+
if (!updated) {
|
|
26
|
+
return token;
|
|
27
|
+
}
|
|
28
28
|
await tokenStorage.saveTokens({
|
|
29
|
-
token,
|
|
30
|
-
|
|
29
|
+
accessToken: token,
|
|
30
|
+
offlineToken: refreshToken ?? undefined,
|
|
31
31
|
idToken: idToken ?? undefined,
|
|
32
32
|
});
|
|
33
33
|
return token;
|
|
@@ -45,7 +45,7 @@ export class KeycloakTokenProvider {
|
|
|
45
45
|
return true;
|
|
46
46
|
}
|
|
47
47
|
const tokens = await tokenStorage.getTokens();
|
|
48
|
-
return !!tokens?.refreshToken;
|
|
48
|
+
return !!(tokens?.offlineToken || tokens?.refreshToken);
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
51
|
console.error('[KeycloakTokenProvider] Has refresh token error:', error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KeycloakTokenProvider.js","sourceRoot":"","sources":["../../../src/axios/adapters/KeycloakTokenProvider.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,YAAY,EAAC,MAAM,4BAA4B,CAAC;AAuBxD,MAAM,OAAO,qBAAqB;IAGhC,YAAY,cAAyC;QACnD,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;IAMD,KAAK,CAAC,QAAQ;QACZ,IAAI,CAAC;YAEH,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;YACxC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,KAAK,CAAC;YACf,CAAC;YAGD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,SAAS,EAAE,CAAC;YAC9C,OAAO,MAAM,EAAE,KAAK,IAAI,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"KeycloakTokenProvider.js","sourceRoot":"","sources":["../../../src/axios/adapters/KeycloakTokenProvider.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,YAAY,EAAC,MAAM,4BAA4B,CAAC;AAuBxD,MAAM,OAAO,qBAAqB;IAGhC,YAAY,cAAyC;QACnD,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;IAMD,KAAK,CAAC,QAAQ;QACZ,IAAI,CAAC;YAEH,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;YACxC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,KAAK,CAAC;YACf,CAAC;YAGD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,SAAS,EAAE,CAAC;YAC9C,OAAO,MAAM,EAAE,WAAW,IAAI,MAAM,EAAE,KAAK,IAAI,IAAI,CAAC;QACtD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,0CAA0C,EAAE,KAAK,CAAC,CAAC;YACjE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IASD,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;YAG1D,MAAM,EAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAC,GAAG,IAAI,CAAC,cAAc,CAAC;YAC3D,IAAI,KAAK,EAAE,CAAC;gBACV,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,KAAK,CAAC;gBACf,CAAC;gBAED,MAAM,YAAY,CAAC,UAAU,CAAC;oBAC5B,WAAW,EAAE,KAAK;oBAClB,YAAY,EAAE,YAAY,IAAI,SAAS;oBACvC,OAAO,EAAE,OAAO,IAAI,SAAS;iBAC9B,CAAC,CAAC;gBACH,OAAO,KAAK,CAAC;YACf,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;YACrE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAKD,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC;YAEH,IAAI,IAAI,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC;gBACrC,OAAO,IAAI,CAAC;YACd,CAAC;YAGD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,SAAS,EAAE,CAAC;YAC9C,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,YAAY,IAAI,MAAM,EAAE,YAAY,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kDAAkD,EAAE,KAAK,CAAC,CAAC;YACzE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CAOF"}
|
|
@@ -35,7 +35,7 @@ export const setupAxiosInterceptors = (axiosInstance, config = {}) => {
|
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
37
|
const tokens = await tokenStorage.getTokens();
|
|
38
|
-
token = tokens?.token || null;
|
|
38
|
+
token = tokens?.accessToken || tokens?.token || null;
|
|
39
39
|
}
|
|
40
40
|
if (token) {
|
|
41
41
|
const formattedToken = tokenProvider?.formatToken?.(token) || `Bearer ${token}`;
|
|
@@ -96,7 +96,7 @@ export const setupAxiosInterceptors = (axiosInstance, config = {}) => {
|
|
|
96
96
|
}
|
|
97
97
|
else {
|
|
98
98
|
const tokens = await tokenStorage.getTokens();
|
|
99
|
-
if (!tokens?.refreshToken) {
|
|
99
|
+
if (!tokens?.offlineToken && !tokens?.refreshToken) {
|
|
100
100
|
throw new Error('No refresh token available');
|
|
101
101
|
}
|
|
102
102
|
console.warn('[AxiosInterceptors] Using deprecated built-in refresh logic. Consider using tokenProvider instead.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interceptors.js","sourceRoot":"","sources":["../../src/axios/interceptors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,iBAAiB,EAAC,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAC,YAAY,EAAC,MAAM,yBAAyB,CAAC;AAIrD,MAAM,YAAY,GAAG,EAAC,OAAO,EAAE,KAAK,EAAC,CAAC;AACtC,MAAM,WAAW,GAGZ,EAAE,CAAC;AAKR,MAAM,YAAY,GAAG,CAAC,KAAmB,EAAE,QAAuB,IAAI,EAAE,EAAE;IACxE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QAC5B,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;aAAM,IAAI,KAAK,EAAE,CAAC;YACjB,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC;AACzB,CAAC,CAAC;AAKF,MAAM,aAAa,GAAG,CAAC,GAAuB,EAAE,mBAA6B,EAAE,EAAW,EAAE;IAC1F,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,OAAO,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACnE,CAAC,CAAC;AAmCF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,aAA4B,EAC5B,SAAkC,EAAE,EACf,EAAE;IACvB,MAAM,EACJ,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,UAAU,GAAG,CAAC,EACd,YAAY,GAAG,IAAI,EACnB,cAAc,GAAG,IAAI,EACrB,gBAAgB,GAAG,EAAE,EACrB,aAAa,EACb,QAAQ,EACR,YAAY,GACb,GAAG,MAAM,CAAC;IAGX,MAAM,oBAAoB,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CACjE,KAAK,EAAE,aAAyC,EAAE,EAAE;QAElD,IAAI,aAAa,CAAC,aAAa,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE,CAAC;YACvD,OAAO,aAAa,CAAC;QACvB,CAAC;QAGD,IAAI,YAAY,EAAE,CAAC;YACjB,IAAI,CAAC;gBACH,IAAI,KAAK,GAAkB,IAAI,CAAC;gBAGhC,IAAI,aAAa,EAAE,CAAC;oBAClB,KAAK,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,CAAC;gBACzC,CAAC;qBAAM,IAAI,QAAQ,EAAE,CAAC;oBACpB,KAAK,GAAG,MAAM,QAAQ,EAAE,CAAC;gBAC3B,CAAC;qBAAM,CAAC;oBACN,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,SAAS,EAAE,CAAC;oBAC9C,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"interceptors.js","sourceRoot":"","sources":["../../src/axios/interceptors.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import React, { type ReactNode } from 'react';
|
|
2
|
-
import type { KeycloakConfig, KeycloakConfigWith2FA } from '../core
|
|
2
|
+
import type { BackgroundReauthConfig, InitRecoveryStrategy, KeycloakConfig, KeycloakConfigWith2FA } from '../core';
|
|
3
|
+
interface KeycloakConfigContextValue {
|
|
4
|
+
config: KeycloakConfig;
|
|
5
|
+
isConfigReady: boolean;
|
|
6
|
+
offlineAccessEnabled: boolean;
|
|
7
|
+
backgroundReauth: BackgroundReauthConfig;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
getClientSecret?: () => string | undefined | Promise<string | undefined>;
|
|
10
|
+
initRecoveryStrategy: InitRecoveryStrategy;
|
|
11
|
+
deferStoredTokensUntilRefresh: boolean;
|
|
12
|
+
refreshPinCheck: () => Promise<void>;
|
|
13
|
+
}
|
|
3
14
|
export interface KeycloakConfigProviderProps {
|
|
4
15
|
children: ReactNode;
|
|
5
16
|
config: KeycloakConfigWith2FA;
|
|
17
|
+
offlineAccessEnabled?: boolean;
|
|
18
|
+
backgroundReauth?: BackgroundReauthConfig;
|
|
19
|
+
clientSecret?: string;
|
|
20
|
+
getClientSecret?: () => string | undefined | Promise<string | undefined>;
|
|
21
|
+
initRecoveryStrategy?: InitRecoveryStrategy;
|
|
22
|
+
deferStoredTokensUntilRefresh?: boolean;
|
|
6
23
|
}
|
|
7
24
|
export declare const KeycloakConfigProvider: React.NamedExoticComponent<KeycloakConfigProviderProps>;
|
|
8
25
|
export declare const useKeycloakConfig: () => KeycloakConfig;
|
|
26
|
+
export declare const useKeycloakSessionOptions: () => Pick<KeycloakConfigContextValue, "offlineAccessEnabled" | "isConfigReady" | "backgroundReauth" | "clientSecret" | "getClientSecret" | "initRecoveryStrategy" | "deferStoredTokensUntilRefresh">;
|
|
9
27
|
export declare const useKeycloakConfigRefresh: () => (() => Promise<void>);
|
|
28
|
+
export {};
|
|
10
29
|
//# sourceMappingURL=KeycloakConfigContext.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KeycloakConfigContext.d.ts","sourceRoot":"","sources":["../../src/context/KeycloakConfigContext.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAEZ,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"KeycloakConfigContext.d.ts","sourceRoot":"","sources":["../../src/context/KeycloakConfigContext.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAEZ,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAGjB,UAAU,0BAA0B;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACzE,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,6BAA6B,EAAE,OAAO,CAAC;IAEvC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAID,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,SAAS,CAAC;IAEpB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACzE,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,6BAA6B,CAAC,EAAE,OAAO,CAAC;CACzC;AAiBD,eAAO,MAAM,sBAAsB,yDAkFlC,CAAC;AASF,eAAO,MAAM,iBAAiB,QAAO,cAQpC,CAAC;AAEF,eAAO,MAAM,yBAAyB,QAAO,IAAI,CAC/C,0BAA0B,EACxB,sBAAsB,GACtB,eAAe,GACf,kBAAkB,GAClB,cAAc,GACd,iBAAiB,GACjB,sBAAsB,GACtB,+BAA+B,CAiBlC,CAAC;AAKF,eAAO,MAAM,wBAAwB,QAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAM/D,CAAC"}
|
|
@@ -11,14 +11,22 @@ function getEffectiveConfig(config, pinSet) {
|
|
|
11
11
|
...(config.oidcProvider != null && { oidcProvider: config.oidcProvider }),
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
-
export const KeycloakConfigProvider = React.memo(({ children, config }) => {
|
|
14
|
+
export const KeycloakConfigProvider = React.memo(({ children, config, offlineAccessEnabled, backgroundReauth, clientSecret, getClientSecret, initRecoveryStrategy, deferStoredTokensUntilRefresh, }) => {
|
|
15
15
|
const [pinSet, setPinSet] = useState(null);
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
let mounted = true;
|
|
18
|
-
credentialStorage
|
|
18
|
+
credentialStorage
|
|
19
|
+
.hasPIN()
|
|
20
|
+
.then(hasPin => {
|
|
19
21
|
if (mounted) {
|
|
20
22
|
setPinSet(hasPin);
|
|
21
23
|
}
|
|
24
|
+
})
|
|
25
|
+
.catch(error => {
|
|
26
|
+
console.error('[KeycloakConfigProvider] PIN check error:', error);
|
|
27
|
+
if (mounted) {
|
|
28
|
+
setPinSet(false);
|
|
29
|
+
}
|
|
22
30
|
});
|
|
23
31
|
return () => {
|
|
24
32
|
mounted = false;
|
|
@@ -29,7 +37,33 @@ export const KeycloakConfigProvider = React.memo(({ children, config }) => {
|
|
|
29
37
|
setPinSet(hasPin);
|
|
30
38
|
}, []);
|
|
31
39
|
const effectiveConfig = useMemo(() => getEffectiveConfig(config, pinSet), [config, pinSet]);
|
|
32
|
-
const
|
|
40
|
+
const resolvedOfflineAccessEnabled = offlineAccessEnabled ?? config.offlineAccessEnabled ?? true;
|
|
41
|
+
const resolvedClientSecret = clientSecret ?? config.clientSecret;
|
|
42
|
+
const resolvedGetClientSecret = getClientSecret ?? config.getClientSecret;
|
|
43
|
+
const resolvedInitRecoveryStrategy = initRecoveryStrategy ?? 'none';
|
|
44
|
+
const resolvedDeferStoredTokensUntilRefresh = deferStoredTokensUntilRefresh ?? false;
|
|
45
|
+
const resolvedBackgroundReauth = useMemo(() => backgroundReauth ?? config.backgroundReauth ?? {}, [backgroundReauth, config.backgroundReauth]);
|
|
46
|
+
const value = useMemo(() => ({
|
|
47
|
+
config: effectiveConfig,
|
|
48
|
+
isConfigReady: pinSet !== null,
|
|
49
|
+
offlineAccessEnabled: resolvedOfflineAccessEnabled,
|
|
50
|
+
backgroundReauth: resolvedBackgroundReauth,
|
|
51
|
+
clientSecret: resolvedClientSecret,
|
|
52
|
+
getClientSecret: resolvedGetClientSecret,
|
|
53
|
+
initRecoveryStrategy: resolvedInitRecoveryStrategy,
|
|
54
|
+
deferStoredTokensUntilRefresh: resolvedDeferStoredTokensUntilRefresh,
|
|
55
|
+
refreshPinCheck,
|
|
56
|
+
}), [
|
|
57
|
+
effectiveConfig,
|
|
58
|
+
pinSet,
|
|
59
|
+
resolvedOfflineAccessEnabled,
|
|
60
|
+
resolvedBackgroundReauth,
|
|
61
|
+
resolvedClientSecret,
|
|
62
|
+
resolvedGetClientSecret,
|
|
63
|
+
resolvedInitRecoveryStrategy,
|
|
64
|
+
resolvedDeferStoredTokensUntilRefresh,
|
|
65
|
+
refreshPinCheck,
|
|
66
|
+
]);
|
|
33
67
|
return (<KeycloakConfigContext.Provider value={value}>{children}</KeycloakConfigContext.Provider>);
|
|
34
68
|
});
|
|
35
69
|
KeycloakConfigProvider.displayName = 'KeycloakConfigProvider';
|
|
@@ -40,6 +74,21 @@ export const useKeycloakConfig = () => {
|
|
|
40
74
|
}
|
|
41
75
|
return context.config;
|
|
42
76
|
};
|
|
77
|
+
export const useKeycloakSessionOptions = () => {
|
|
78
|
+
const context = useContext(KeycloakConfigContext);
|
|
79
|
+
if (!context) {
|
|
80
|
+
throw new Error('useKeycloakSessionOptions must be used within KeycloakConfigProvider');
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
offlineAccessEnabled: context.offlineAccessEnabled,
|
|
84
|
+
isConfigReady: context.isConfigReady,
|
|
85
|
+
backgroundReauth: context.backgroundReauth,
|
|
86
|
+
clientSecret: context.clientSecret,
|
|
87
|
+
getClientSecret: context.getClientSecret,
|
|
88
|
+
initRecoveryStrategy: context.initRecoveryStrategy,
|
|
89
|
+
deferStoredTokensUntilRefresh: context.deferStoredTokensUntilRefresh,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
43
92
|
export const useKeycloakConfigRefresh = () => {
|
|
44
93
|
const context = useContext(KeycloakConfigContext);
|
|
45
94
|
if (!context) {
|