@bmc-soft/keycloak-auth 1.0.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 +539 -0
- package/THEMING.md +116 -0
- package/package.json +98 -0
package/README.md
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# @bmc-soft/keycloak-auth
|
|
2
|
+
|
|
3
|
+
Готовый к продакшену пакет аутентификации Keycloak для React Native с упором на производительность и безопасность.
|
|
4
|
+
|
|
5
|
+
## Возможности
|
|
6
|
+
|
|
7
|
+
- **Производительность**: разбиение контекстов снижает лишние ре-рендеры; обновление токенов не затрагивает компоненты, которым нужны только конфиг или инстанс.
|
|
8
|
+
- **Безопасность**: токены в OS Keychain; PIN шифруется AES; учётные данные не хранятся в открытом виде.
|
|
9
|
+
- **Готовые экраны**: AuthPage, ConfirmAuthPage, виджеты ReauthBottomSheet и кнопки выхода.
|
|
10
|
+
- **Темизация**: настройка через тему в `KeycloakProvider` (цвета, шрифты, Loader, кнопки).
|
|
11
|
+
- **Автообновление токена**: обновление с повтором запроса; при ошибке — опциональный `onReauthRequired`.
|
|
12
|
+
- **Биометрия**: FaceID/TouchID для подтверждения по PIN.
|
|
13
|
+
- **React Native**: встроенные полифиллы для keycloak-js, без дополнительной настройки.
|
|
14
|
+
|
|
15
|
+
## Установка
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @bmc-soft/keycloak-auth
|
|
19
|
+
# или
|
|
20
|
+
yarn add @bmc-soft/keycloak-auth
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Peer-зависимости
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install react-native-keychain
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Настройка нативных модулей: [react-native-keychain](https://github.com/oblador/react-native-keychain).
|
|
30
|
+
|
|
31
|
+
Опционально (для интерцепторов): `axios` (>=1.0.0).
|
|
32
|
+
Для UI нужны: `@gorhom/bottom-sheet`, `react-native-webview`, `react-native-safe-area-context`, `lottie-react-native`, `react-native-svg`.
|
|
33
|
+
|
|
34
|
+
> Полифиллы для React Native подключаются автоматически при использовании пакета; отдельная настройка keycloak-js не требуется.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Быстрый старт
|
|
39
|
+
|
|
40
|
+
### 1. Оборачиваем приложение в KeycloakProvider
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { KeycloakProvider } from '@bmc-soft/keycloak-auth';
|
|
44
|
+
|
|
45
|
+
const App = () => (
|
|
46
|
+
<KeycloakProvider
|
|
47
|
+
config={{
|
|
48
|
+
url: 'https://your-keycloak-server.com',
|
|
49
|
+
realm: 'your-realm',
|
|
50
|
+
clientId: 'your-client-id',
|
|
51
|
+
}}
|
|
52
|
+
redirectUri="myapp://callback"
|
|
53
|
+
>
|
|
54
|
+
<YourApp />
|
|
55
|
+
</KeycloakProvider>
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Экран входа
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { AuthPage } from '@bmc-soft/keycloak-auth';
|
|
63
|
+
|
|
64
|
+
const LoginScreen = () => (
|
|
65
|
+
<AuthPage
|
|
66
|
+
logo={require('./logo.png')}
|
|
67
|
+
onSuccess={(token) => {
|
|
68
|
+
// Сохраняем токен в сессию приложения и переходим
|
|
69
|
+
saveToken(token);
|
|
70
|
+
navigation.replace('Home');
|
|
71
|
+
}}
|
|
72
|
+
onError={(err) => console.error(err)}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. (Опционально) Настройка интерцепторов Axios
|
|
78
|
+
|
|
79
|
+
Если используете axios, задайте провайдер токенов (см. [Axios](#axios)) и вызовите `setupAxiosInterceptors` с `tokenProvider` и `onReauthRequired`. Провайдер обычно настраивается внутри KeycloakProvider после инициализации keycloak (см. [Интеграция](#интеграция)).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Провайдер и конфигурация
|
|
84
|
+
|
|
85
|
+
### KeycloakProvider
|
|
86
|
+
|
|
87
|
+
| Проп | Тип | Обязательный | Описание |
|
|
88
|
+
|------|-----|--------------|----------|
|
|
89
|
+
| `children` | ReactNode | да | Дерево приложения |
|
|
90
|
+
| `config` | KeycloakConfigWith2FA | да | `url`, `realm`, `clientId`; опционально `clientId2fa` для первого входа с 2FA |
|
|
91
|
+
| `redirectUri` | string | да | URI OAuth callback (например `myapp://callback`) |
|
|
92
|
+
| `theme` | KeycloakTheme | нет | Цвета, шрифты, LoaderComponent, компоненты кнопок |
|
|
93
|
+
| `onTokens` | (tokens: KeycloakTokens) => void | нет | Вызывается при изменении токенов (логин, refresh, logout) |
|
|
94
|
+
| `autoRefreshToken` | boolean | нет | По умолчанию `true` |
|
|
95
|
+
| `autoRefreshTokenMinValidity` | number | нет | За сколько секунд до истечения вызывать refresh; по умолчанию `5` |
|
|
96
|
+
| `onReauthRequired` | () => void | нет | Вызывается при ошибке обновления токена (например показать экран реавторизации) |
|
|
97
|
+
|
|
98
|
+
### KeycloakTheme
|
|
99
|
+
|
|
100
|
+
Все поля опциональны; недостающие подставляются из дефолтной темы. Передаётся в KeycloakProvider как `theme`.
|
|
101
|
+
|
|
102
|
+
- **fonts**: `primary`, `heading`
|
|
103
|
+
- **colors**: `primary`, `background`, `error`, `text`, `button`, `buttonText`, `link`, `outlinedButtonBackground`, `outlinedButtonText`, `numberPadButtonBackground`, `numberPadText`, `numberPadDisabled`, `pinIndicatorEmpty`, `border`, `success`
|
|
104
|
+
- **LoaderComponent**: компонент состояния загрузки
|
|
105
|
+
- **ContainedButtonComponent**, **OutlinedButtonComponent**, **IconButtonComponent**: компоненты кнопок (пропсы см. в THEMING.md)
|
|
106
|
+
|
|
107
|
+
Полная структура и примеры: [THEMING.md](./THEMING.md).
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Компоненты
|
|
112
|
+
|
|
113
|
+
### Экраны
|
|
114
|
+
|
|
115
|
+
#### AuthPage
|
|
116
|
+
|
|
117
|
+
Первый вход: логин в WebView → установка PIN → обмен code на токены. Используется на экране «Вход».
|
|
118
|
+
|
|
119
|
+
| Проп | Тип | По умолчанию | Описание |
|
|
120
|
+
|------|-----|--------------|----------|
|
|
121
|
+
| `onSuccess` | (token: string) => void | — | После успешного обмена code; передаётся access token |
|
|
122
|
+
| `onError` | (error: Error) => void | — | При ошибке обмена или сохранения |
|
|
123
|
+
| `logo` | ReactNode \| ImageSourcePropType | — | Логотип приложения |
|
|
124
|
+
| `logoHeight`, `logoWidth` | number | 80 | Размер логотипа при использовании image source |
|
|
125
|
+
| `showBiometryPrompt` | boolean | true | Спрашивать биометрию после установки PIN |
|
|
126
|
+
| `pinLength` | number | 4 | Длина PIN |
|
|
127
|
+
| `style`, `paddingTop`, `paddingBottom` | ViewStyle / number | — | Стили контейнера |
|
|
128
|
+
|
|
129
|
+
#### ConfirmAuthPage
|
|
130
|
+
|
|
131
|
+
Подтверждение сессии: фазы webview_detect → pin → webview_with_credentials (если сохранены учётные данные). Используется на экране «Подтверждение PIN» и внутри BottomSheet реавторизации.
|
|
132
|
+
|
|
133
|
+
| Проп | Тип | По умолчанию | Описание |
|
|
134
|
+
|------|-----|--------------|----------|
|
|
135
|
+
| `onSuccess` | () => void | — | Успех (редирект или PIN без credentials) |
|
|
136
|
+
| `onError` | (error: Error) => void | — | Например неверный PIN |
|
|
137
|
+
| `onLogout` | () => void | — | После очистки хранилища пакета; в приложении — очистить сессию и перейти на экран входа |
|
|
138
|
+
| `logoutText` | string | "Выйти из аккаунта" | Текст ссылки «Выйти» |
|
|
139
|
+
| `logo`, `logoHeight`, `logoWidth` | — | 80 | Логотип |
|
|
140
|
+
| `pinLength` | number | 4 | Длина PIN |
|
|
141
|
+
| `allowBiometry`, `autoShowBiometry` | boolean | true | Биометрия |
|
|
142
|
+
| `title`, `description` | string | — | Заголовок и описание блока PIN |
|
|
143
|
+
| `style` | ViewStyle | — | Стиль контейнера |
|
|
144
|
+
| `webViewTimeoutMs` | number | 3000 | Задержка (мс) в фазе webview_detect перед переходом на PIN |
|
|
145
|
+
|
|
146
|
+
### Виджеты
|
|
147
|
+
|
|
148
|
+
#### ReauthBottomSheet
|
|
149
|
+
|
|
150
|
+
BottomSheet с подтверждением PIN для реавторизации. Управляется только пропсами (не привязан к Session приложения).
|
|
151
|
+
|
|
152
|
+
| Проп | Тип | По умолчанию | Описание |
|
|
153
|
+
|------|-----|--------------|----------|
|
|
154
|
+
| `isVisible` | boolean | — | Отображать ли sheet |
|
|
155
|
+
| `onSuccess` | () => void | — | Реавторизация прошла успешно |
|
|
156
|
+
| `onDismiss` | () => void | — | Закрытие без успеха |
|
|
157
|
+
| `onError` | (error: Error) => void | — | Ошибка ввода PIN |
|
|
158
|
+
| `pinLength` | number | 4 | Длина PIN |
|
|
159
|
+
| `allowBiometry`, `autoShowBiometry` | boolean | true | Биометрия |
|
|
160
|
+
| `title` | string | "Подтвердите вход" | Заголовок |
|
|
161
|
+
| `description` | string | "Введите PIN-код" | Описание |
|
|
162
|
+
| `footer` | ReactNode | — | Опциональный футер |
|
|
163
|
+
| `snapPointPercentage` | number | 50 | Высота sheet в % |
|
|
164
|
+
|
|
165
|
+
### Кнопки выхода
|
|
166
|
+
|
|
167
|
+
#### LogoutButtonText
|
|
168
|
+
|
|
169
|
+
Текстовая кнопка (contained или outlined). По нажатию открывается полноэкранный Modal с WebViewLogout. Должна использоваться внутри KeycloakProvider.
|
|
170
|
+
|
|
171
|
+
- **Базовые**: `onLogoutSuccess`, `onError`, `style`
|
|
172
|
+
- **Специфичные**: `variant: 'contained' | 'outlined'`, `label: string`
|
|
173
|
+
|
|
174
|
+
#### LogoutButtonIcon
|
|
175
|
+
|
|
176
|
+
Кнопка выхода только с иконкой. Базовые пропсы плюс `renderIcon: ReactNode`.
|
|
177
|
+
|
|
178
|
+
### Остальные UI (внутри пакета или для кастомных сценариев)
|
|
179
|
+
|
|
180
|
+
- **WebViewLogin**, **WebViewLogout** — OAuth-потоки в WebView
|
|
181
|
+
- **PINSetup**, **PINConfirm** — ввод и подтверждение PIN
|
|
182
|
+
- **NumberPad**, **PINIndicator** — UI ввода PIN
|
|
183
|
+
- **LogoutConfirmSheet** — подтверждение выхода
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Хуки
|
|
188
|
+
|
|
189
|
+
Все хуки должны вызываться внутри KeycloakProvider (если не указано иное).
|
|
190
|
+
|
|
191
|
+
### useKeycloakAuth()
|
|
192
|
+
|
|
193
|
+
Полный API авторизации: инстанс keycloak, токен, login, logout, URL, состояние реавторизации.
|
|
194
|
+
|
|
195
|
+
Возвращает: `keycloak`, `isInitialized`, `isLoading`, `error`, `token`, `isExpired`, `isAuthenticated`, `updateToken`, `login`, `logout`, `loadUserProfile`, `createLoginUrl`, `createLogoutUrl`, `clearTokens`, `isReauthRequired`, `showReauth`, `hideReauth`.
|
|
196
|
+
|
|
197
|
+
### useToken()
|
|
198
|
+
|
|
199
|
+
Доступ только к токенам (меньше ре-рендеров). Возвращает: `token`, `refreshToken`, `idToken`, `isExpired`, `updateToken`, `clearTokens`.
|
|
200
|
+
|
|
201
|
+
### useReauth()
|
|
202
|
+
|
|
203
|
+
Состояние UI реавторизации. Возвращает: `isReauthRequired`, `showReauth`, `hideReauth`.
|
|
204
|
+
|
|
205
|
+
### useKeycloakAuthScreen(options?)
|
|
206
|
+
|
|
207
|
+
Определяет, какой экран авторизации показывать: `'login'` или `'confirm'`. Используется для `initialRouteName` в Auth Stack.
|
|
208
|
+
|
|
209
|
+
Возвращает: `screen: 'login' | 'confirm' | null`, `isLoading`, `error`.
|
|
210
|
+
|
|
211
|
+
Опции: `shouldShowConfirm?: () => Promise<boolean>` — переопределить стандартную проверку (наличие токенов и PIN).
|
|
212
|
+
|
|
213
|
+
### useKeycloakTheme()
|
|
214
|
+
|
|
215
|
+
Текущая тема (объединённая с дефолтной). Возвращает: `fonts`, `colors`, `LoaderComponent`, `ContainedButtonComponent`, `OutlinedButtonComponent`, `IconButtonComponent`. Можно вызывать вне провайдера (вернётся дефолтная тема).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Хранилище
|
|
220
|
+
|
|
221
|
+
### tokenStorage
|
|
222
|
+
|
|
223
|
+
Хранение токенов в Keychain. API: `getToken`, `saveToken`, `getRefreshToken`, `saveRefreshToken`, `getIdToken`, `saveIdToken`, `getTokens`, `saveTokens`, `clearTokens`, `hasTokens`.
|
|
224
|
+
|
|
225
|
+
После успешного ConfirmAuthPage (например при реавторизации) вызовите `tokenStorage.getToken()`, чтобы получить текущий access token и обновить сессию приложения.
|
|
226
|
+
|
|
227
|
+
> **Важно**: не храните токены в AsyncStorage. Используйте только Keychain через пакет.
|
|
228
|
+
|
|
229
|
+
### credentialStorage
|
|
230
|
+
|
|
231
|
+
Используется внутри пакета для зашифрованных учётных данных и биометрии. Экспортируется для продвинутых сценариев (например очистка PIN/credentials при выходе).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Axios
|
|
236
|
+
|
|
237
|
+
### Интерфейс TokenProvider
|
|
238
|
+
|
|
239
|
+
Используется интерцепторами для получения и обновления токенов:
|
|
240
|
+
|
|
241
|
+
- `getToken(): Promise<string | null> | string | null`
|
|
242
|
+
- `refreshToken(): Promise<string | null>`
|
|
243
|
+
- `hasRefreshToken?(): Promise<boolean> | boolean` (опционально)
|
|
244
|
+
- `formatToken?(token: string): string` (опционально; по умолчанию `Bearer ${token}`)
|
|
245
|
+
|
|
246
|
+
### setupAxiosInterceptors(axiosInstance, config)
|
|
247
|
+
|
|
248
|
+
Добавляет интерцепторы: в запрос — подстановка токена; в ответ — retry при 401, обновление токена, при ошибке — вызов `onReauthRequired`.
|
|
249
|
+
|
|
250
|
+
**config**: `tokenProvider`, `onReauthRequired`, `onTokenRefreshed`, `onRefreshError`, `maxRetries` (по умолчанию 1), `autoAddToken` (true), `autoRetryOn401` (true), `excludeEndpoints`.
|
|
251
|
+
|
|
252
|
+
Возвращает `{ cleanup }` для снятия интерцепторов.
|
|
253
|
+
|
|
254
|
+
### KeycloakTokenProvider
|
|
255
|
+
|
|
256
|
+
Класс, реализующий TokenProvider на основе инстанса KeycloakReactNativeClient. Удобно, когда инстанс клиента уже есть и нужен адаптер для интерцепторов.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Интеграция
|
|
261
|
+
|
|
262
|
+
Пошаговая интеграция с примерами кода. Рассматривается только Keycloak.
|
|
263
|
+
|
|
264
|
+
### 1. Оборачивание приложения в KeycloakProvider
|
|
265
|
+
|
|
266
|
+
Конфиг и `redirectUri` берут из настроек приложения. Тему (Loader, кнопки, цвета) передайте из темы приложения. В `onTokens` сохраняйте токен в хранилище сессии; в `onReauthRequired` показывайте экран реавторизации.
|
|
267
|
+
|
|
268
|
+
**Порядок провайдеров**: если используется `BottomSheetModalProvider` из `@gorhom/bottom-sheet` (например для реавторизации или других модалок), он должен находиться **внутри** KeycloakProvider. Иначе ReauthBottomSheet или ConfirmAuthPage внутри модалки не получат контекст Keycloak. Порядок: `KeycloakProvider` → `BottomSheetModalProvider` → остальное дерево приложения.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
|
272
|
+
import { KeycloakProvider, type KeycloakTheme } from '@bmc-soft/keycloak-auth';
|
|
273
|
+
|
|
274
|
+
const theme: KeycloakTheme = {
|
|
275
|
+
colors: {
|
|
276
|
+
primary: appColors.primary,
|
|
277
|
+
background: appColors.background,
|
|
278
|
+
error: appColors.error,
|
|
279
|
+
text: appColors.text,
|
|
280
|
+
numberPadButtonBackground: appColors.cardBackground,
|
|
281
|
+
numberPadText: appColors.text,
|
|
282
|
+
pinIndicatorEmpty: appColors.textMuted,
|
|
283
|
+
},
|
|
284
|
+
LoaderComponent: AppLoader,
|
|
285
|
+
ContainedButtonComponent: AppContainedButton,
|
|
286
|
+
OutlinedButtonComponent: AppOutlinedButton,
|
|
287
|
+
IconButtonComponent: AppIconButton,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const App = () => (
|
|
291
|
+
<KeycloakProvider
|
|
292
|
+
config={{
|
|
293
|
+
url: 'https://sso.example.com',
|
|
294
|
+
realm: 'my-realm',
|
|
295
|
+
clientId: 'my-app',
|
|
296
|
+
}}
|
|
297
|
+
redirectUri="myapp://callback"
|
|
298
|
+
theme={theme}
|
|
299
|
+
onTokens={({ token }) => {
|
|
300
|
+
Session.events.onChangeAuthToken(token);
|
|
301
|
+
}}
|
|
302
|
+
onReauthRequired={() => {
|
|
303
|
+
Session.events.onRequireReauth();
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
<BottomSheetModalProvider>
|
|
307
|
+
<RootNavigator />
|
|
308
|
+
</BottomSheetModalProvider>
|
|
309
|
+
</KeycloakProvider>
|
|
310
|
+
);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 2. Установка TokenProvider для axios
|
|
314
|
+
|
|
315
|
+
Провайдер токенов настраивается один раз внутри KeycloakProvider после инициализации keycloak (например во вспомогательном компоненте). Не дублируйте настройку на экране логина.
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
import { useKeycloakAuth } from '@bmc-soft/keycloak-auth';
|
|
319
|
+
import { setKeycloakTokenProvider, resetKeycloakTokenProvider } from './axios';
|
|
320
|
+
|
|
321
|
+
const KeycloakAxiosTokenProviderSetup = ({ children }) => {
|
|
322
|
+
const { keycloak, isInitialized } = useKeycloakAuth();
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (isInitialized && keycloak) {
|
|
326
|
+
setKeycloakTokenProvider({
|
|
327
|
+
getToken: () => keycloak.token || null,
|
|
328
|
+
updateToken: async (minValidity) => {
|
|
329
|
+
const result = await keycloak.updateToken(minValidity);
|
|
330
|
+
return result !== null;
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return () => resetKeycloakTokenProvider();
|
|
335
|
+
}, [isInitialized, keycloak]);
|
|
336
|
+
|
|
337
|
+
return <>{children}</>;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Внутри KeycloakProvider:
|
|
341
|
+
<KeycloakProvider config={...} redirectUri={...} ...>
|
|
342
|
+
<KeycloakAxiosTokenProviderSetup>
|
|
343
|
+
<BottomSheetModalProvider>
|
|
344
|
+
<RootNavigator />
|
|
345
|
+
</BottomSheetModalProvider>
|
|
346
|
+
</KeycloakAxiosTokenProviderSetup>
|
|
347
|
+
</KeycloakProvider>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Подключение интерцепторов к инстансу axios (там, где он создаётся):
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { setupAxiosInterceptors } from '@bmc-soft/keycloak-auth';
|
|
354
|
+
import { getKeycloakTokenProvider } from './keycloakTokenProvider';
|
|
355
|
+
|
|
356
|
+
const api = axios.create({ baseURL: 'https://api.example.com' });
|
|
357
|
+
|
|
358
|
+
setupAxiosInterceptors(api, {
|
|
359
|
+
tokenProvider: {
|
|
360
|
+
getToken: () => getKeycloakTokenProvider()?.getToken() ?? null,
|
|
361
|
+
refreshToken: () =>
|
|
362
|
+
getKeycloakTokenProvider()
|
|
363
|
+
?.updateToken(120)
|
|
364
|
+
.then((ok) => (ok ? getKeycloakTokenProvider()?.getToken() ?? null : null)),
|
|
365
|
+
},
|
|
366
|
+
onReauthRequired: () => Session.events.onRequireReauth(),
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 3. Стек авторизации (Login + Confirm)
|
|
371
|
+
|
|
372
|
+
Два экрана: Login (AuthPage) и Confirm (ConfirmAuthPage). Начальный маршрут задаётся через `useKeycloakAuthScreen()`: показывать Confirm, если есть токены и PIN, иначе Login.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
import { useKeycloakAuthScreen, AuthPage, ConfirmAuthPage, tokenStorage } from '@bmc-soft/keycloak-auth';
|
|
376
|
+
|
|
377
|
+
const Stack = createNativeStackNavigator();
|
|
378
|
+
|
|
379
|
+
export const AuthStack = () => {
|
|
380
|
+
const { screen, isLoading } = useKeycloakAuthScreen();
|
|
381
|
+
|
|
382
|
+
if (isLoading) return <Loader />;
|
|
383
|
+
|
|
384
|
+
const initialRoute = screen === 'confirm' ? 'Confirm' : 'Login';
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<Stack.Navigator initialRouteName={initialRoute}>
|
|
388
|
+
<Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
|
|
389
|
+
<Stack.Screen name="Confirm" component={ConfirmScreen} options={{ headerShown: false }} />
|
|
390
|
+
</Stack.Navigator>
|
|
391
|
+
);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const LoginScreen = () => (
|
|
395
|
+
<AuthPage
|
|
396
|
+
logo={require('./logo.png')}
|
|
397
|
+
onSuccess={(token) => Session.events.onLogin(token)}
|
|
398
|
+
onError={(err) => console.error(err)}
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const ConfirmScreen = () => {
|
|
403
|
+
const navigation = useNavigation();
|
|
404
|
+
|
|
405
|
+
const onSuccess = useCallback(async () => {
|
|
406
|
+
const token = await tokenStorage.getToken();
|
|
407
|
+
if (token) Session.events.onLogin(token);
|
|
408
|
+
}, []);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<ConfirmAuthPage
|
|
412
|
+
onSuccess={onSuccess}
|
|
413
|
+
onLogout={() => {
|
|
414
|
+
Session.events.onLogout();
|
|
415
|
+
navigation.replace('Login');
|
|
416
|
+
}}
|
|
417
|
+
pinLength={4}
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
};
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### 4. Реавторизация при 401
|
|
424
|
+
|
|
425
|
+
При 401 интерцепторы вызывают `onReauthRequired`. Покажите BottomSheet (или полноэкранный экран) с ConfirmAuthPage. После успешного ввода PIN получите токен из пакета, обновите сессию и закройте реавторизацию.
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
import { ConfirmAuthPage, tokenStorage } from '@bmc-soft/keycloak-auth';
|
|
429
|
+
import BottomSheet from '@gorhom/bottom-sheet';
|
|
430
|
+
|
|
431
|
+
export const ReauthBottomSheet = () => {
|
|
432
|
+
const sheetRef = useRef(null);
|
|
433
|
+
const showReauth = useStore($reauthRequired); // например Effector / useState
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
showReauth ? sheetRef.current?.snapToIndex(0) : sheetRef.current?.close();
|
|
437
|
+
}, [showReauth]);
|
|
438
|
+
|
|
439
|
+
const handleSuccess = useCallback(async () => {
|
|
440
|
+
const token = await tokenStorage.getToken();
|
|
441
|
+
if (token) {
|
|
442
|
+
Session.events.onLogin(token);
|
|
443
|
+
Session.events.onReauthCompleted();
|
|
444
|
+
}
|
|
445
|
+
sheetRef.current?.close();
|
|
446
|
+
}, []);
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<BottomSheet ref={sheetRef} snapPoints={['50%']} enablePanDownToClose>
|
|
450
|
+
<ConfirmAuthPage onSuccess={handleSuccess} pinLength={4} />
|
|
451
|
+
</BottomSheet>
|
|
452
|
+
);
|
|
453
|
+
};
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### 5. Выход
|
|
457
|
+
|
|
458
|
+
Используйте LogoutButtonIcon или LogoutButtonText. По нажатию открывается Modal с WebViewLogout; при успешном выходе вызывается `onLogoutSuccess` — там очищайте сессию приложения.
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import { LogoutButtonIcon, LogoutButtonText } from '@bmc-soft/keycloak-auth';
|
|
462
|
+
|
|
463
|
+
export const LogoutButton = ({ variant }: { variant: 'icon' | 'text' }) => {
|
|
464
|
+
const handleLogoutSuccess = useCallback(() => {
|
|
465
|
+
Session.events.onLogout();
|
|
466
|
+
}, []);
|
|
467
|
+
|
|
468
|
+
if (variant === 'icon') {
|
|
469
|
+
return (
|
|
470
|
+
<LogoutButtonIcon
|
|
471
|
+
renderIcon={<Icon name="logout" color={theme.error} />}
|
|
472
|
+
onLogoutSuccess={handleLogoutSuccess}
|
|
473
|
+
/>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<LogoutButtonText
|
|
479
|
+
variant="outlined"
|
|
480
|
+
label="Выйти"
|
|
481
|
+
onLogoutSuccess={handleLogoutSuccess}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
};
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Схема потока
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
KeycloakProvider → инициализация Keycloak → установка TokenProvider для axios
|
|
491
|
+
↓
|
|
492
|
+
useKeycloakAuthScreen → Login (AuthPage) или Confirm (ConfirmAuthPage)
|
|
493
|
+
↓
|
|
494
|
+
onSuccess → Session.onLogin(token) | onLogout → Session.onLogout + навигация
|
|
495
|
+
↓
|
|
496
|
+
axios 401 → onReauthRequired → ReauthBottomSheet с ConfirmAuthPage
|
|
497
|
+
↓
|
|
498
|
+
onSuccess → tokenStorage.getToken() → Session.onLogin + onReauthCompleted → закрыть sheet
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Архитектура и безопасность
|
|
504
|
+
|
|
505
|
+
### Иерархия контекстов
|
|
506
|
+
|
|
507
|
+
```
|
|
508
|
+
KeycloakConfigProvider ← конфиг (редко меняется)
|
|
509
|
+
└─ KeycloakInstanceProvider ← инстанс (один раз)
|
|
510
|
+
└─ KeycloakThemeProvider ← тема (опционально; мемоизирована)
|
|
511
|
+
└─ TokenProvider ← токены (частые обновления)
|
|
512
|
+
└─ ReauthProvider ← состояние реавторизации
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Обновление токенов не вызывает ре-рендер компонентов, зависящих только от конфига, инстанса или темы.
|
|
516
|
+
|
|
517
|
+
### Безопасность
|
|
518
|
+
|
|
519
|
+
- Токены в OS Keychain (не AsyncStorage)
|
|
520
|
+
- PIN шифруется AES перед сохранением
|
|
521
|
+
- Запросы по HTTPS
|
|
522
|
+
- Учётные данные не хранятся в открытом виде
|
|
523
|
+
|
|
524
|
+
### Полифиллы React Native
|
|
525
|
+
|
|
526
|
+
Пакет подключает полифиллы для `document`, `window`, `navigator`, требуемые keycloak-js. Они применяются автоматически при использовании KeycloakProvider. Чтобы применить раньше: `import { initKeycloakPolyfills } from '@bmc-soft/keycloak-auth'; initKeycloakPolyfills();`.
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Экспорты
|
|
531
|
+
|
|
532
|
+
- **Основной**: `@bmc-soft/keycloak-auth` — провайдер, хуки, экраны, виджеты, UI, хранилище, помощники axios, типы
|
|
533
|
+
- **Подпути**: `@bmc-soft/keycloak-auth/screens`, `/widgets`, `/hooks`, `/axios`, `/context`, `/storage` (см. `exports` в package.json)
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Лицензия
|
|
538
|
+
|
|
539
|
+
MIT © Koala Team
|
package/THEMING.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Theming
|
|
2
|
+
|
|
3
|
+
Настройка внешнего вида экранов и виджетов Keycloak через единую тему в `KeycloakProvider`.
|
|
4
|
+
|
|
5
|
+
## Передача темы
|
|
6
|
+
|
|
7
|
+
Передайте объект `theme` в `KeycloakProvider`. Все поля опциональны; не указанные значения берутся из дефолтной темы пакета.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { KeycloakProvider, type KeycloakTheme } from '@bmc-soft/keycloak-auth';
|
|
11
|
+
|
|
12
|
+
const theme: KeycloakTheme = {
|
|
13
|
+
fonts: { primary: 'OpenSans-Regular', heading: 'OpenSans-SemiBold' },
|
|
14
|
+
colors: { background: '#0d0d0d', primary: '#0066cc' },
|
|
15
|
+
LoaderComponent: MyLoader,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
<KeycloakProvider config={config} redirectUri={redirectUri} theme={theme}>
|
|
19
|
+
<App />
|
|
20
|
+
</KeycloakProvider>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Приоритет значений
|
|
24
|
+
|
|
25
|
+
Во всех компонентах пакета действует правило:
|
|
26
|
+
|
|
27
|
+
**проп компонента > тема из контекста > дефолт пакета**
|
|
28
|
+
|
|
29
|
+
Например, в теме задаётся `colors.background` — экраны и виджеты пакета (AuthPage, ConfirmAuthPage, ReauthBottomSheet и др.) используют его для фона. Компоненты, у которых есть соответствующие пропы (например `style`), могут переопределять стили точечно.
|
|
30
|
+
|
|
31
|
+
## Структура KeycloakTheme
|
|
32
|
+
|
|
33
|
+
Тип экспортируется из пакета: `import type { KeycloakTheme } from '@bmc-soft/keycloak-auth'`.
|
|
34
|
+
|
|
35
|
+
| Поле | Тип | Описание |
|
|
36
|
+
|------|-----|----------|
|
|
37
|
+
| `fonts` | `KeycloakThemeFonts` | Шрифты для текста и заголовков |
|
|
38
|
+
| `colors` | `KeycloakThemeColors` | Цвета экранов, кнопок, ошибок и т.д. |
|
|
39
|
+
| `LoaderComponent` | `React.ComponentType` | Компонент индикатора загрузки |
|
|
40
|
+
| `ContainedButtonComponent` | `React.ComponentType<KeycloakThemeButtonProps>` | Кнопка с заливкой |
|
|
41
|
+
| `OutlinedButtonComponent` | `React.ComponentType<KeycloakThemeButtonProps>` | Кнопка с обводкой |
|
|
42
|
+
| `IconButtonComponent` | `React.ComponentType<KeycloakThemeIconButtonProps>` | Иконковая кнопка |
|
|
43
|
+
|
|
44
|
+
### KeycloakThemeFonts
|
|
45
|
+
|
|
46
|
+
- `primary?: string` — основной шрифт (текст)
|
|
47
|
+
- `heading?: string` — шрифт заголовков
|
|
48
|
+
|
|
49
|
+
### KeycloakThemeColors
|
|
50
|
+
|
|
51
|
+
Все поля опциональны. Основные:
|
|
52
|
+
|
|
53
|
+
- `primary` — акцент (ссылки, активный PIN)
|
|
54
|
+
- `background` — фон экранов и контейнеров
|
|
55
|
+
- `error` — ошибки и опасные действия
|
|
56
|
+
- `text` — основной текст
|
|
57
|
+
- `button` / `buttonText` — кнопка с заливкой
|
|
58
|
+
- `link` — ссылки
|
|
59
|
+
- `outlinedButtonBackground` / `outlinedButtonText` — кнопка с обводкой
|
|
60
|
+
- `numberPadButtonBackground` / `numberPadText` / `numberPadDisabled` — клавиатура PIN
|
|
61
|
+
- `pinIndicatorEmpty` — пустые точки PIN-индикатора
|
|
62
|
+
- `border` — разделители
|
|
63
|
+
- `success` — успешное состояние
|
|
64
|
+
|
|
65
|
+
## Пример с приложением
|
|
66
|
+
|
|
67
|
+
Подстановка темы приложения (например, из shared/settings или brandbook):
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { KeycloakProvider, type KeycloakTheme } from '@bmc-soft/keycloak-auth';
|
|
71
|
+
import { useAppTheme } from '@shared/settings';
|
|
72
|
+
import { Loader } from '@shared/ui';
|
|
73
|
+
|
|
74
|
+
function KeycloakAppProvider({ children }) {
|
|
75
|
+
const { colors, fonts } = useAppTheme();
|
|
76
|
+
|
|
77
|
+
const theme: KeycloakTheme = {
|
|
78
|
+
fonts: {
|
|
79
|
+
primary: fonts?.regular,
|
|
80
|
+
heading: fonts?.semiBold,
|
|
81
|
+
},
|
|
82
|
+
colors: {
|
|
83
|
+
primary: colors.primary,
|
|
84
|
+
background: colors.screenBackground,
|
|
85
|
+
error: colors.error,
|
|
86
|
+
text: colors.text,
|
|
87
|
+
},
|
|
88
|
+
LoaderComponent: Loader,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<KeycloakProvider config={config} redirectUri={redirectUri} theme={theme}>
|
|
93
|
+
{children}
|
|
94
|
+
</KeycloakProvider>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Хук useKeycloakTheme()
|
|
100
|
+
|
|
101
|
+
Компоненты пакета получают тему через внутренний контекст. Если вы пишете обёртку или кастомный экран, можно использовать хук:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useKeycloakTheme } from '@bmc-soft/keycloak-auth';
|
|
105
|
+
|
|
106
|
+
function MyWrapper() {
|
|
107
|
+
const { colors, LoaderComponent } = useKeycloakTheme();
|
|
108
|
+
return (
|
|
109
|
+
<View style={{ backgroundColor: colors.background }}>
|
|
110
|
+
<LoaderComponent />
|
|
111
|
+
</View>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Без обёртки `KeycloakProvider` хук возвращает дефолтную тему пакета (не бросает ошибку).
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bmc-soft/keycloak-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready Keycloak authentication package for React Native with optimized performance and security",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"THEMING.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./screens": {
|
|
18
|
+
"types": "./dist/screens/index.d.ts",
|
|
19
|
+
"default": "./dist/screens/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./widgets": {
|
|
22
|
+
"types": "./dist/widgets/index.d.ts",
|
|
23
|
+
"default": "./dist/widgets/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./hooks": {
|
|
26
|
+
"types": "./dist/hooks/index.d.ts",
|
|
27
|
+
"default": "./dist/hooks/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./axios": {
|
|
30
|
+
"types": "./dist/axios/index.d.ts",
|
|
31
|
+
"default": "./dist/axios/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./context": {
|
|
34
|
+
"types": "./dist/context/index.d.ts",
|
|
35
|
+
"default": "./dist/context/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./storage": {
|
|
38
|
+
"types": "./dist/storage/index.d.ts",
|
|
39
|
+
"default": "./dist/storage/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"watch": "tsc --watch",
|
|
45
|
+
"clean": "rm -rf dist",
|
|
46
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"bmc-soft"
|
|
51
|
+
],
|
|
52
|
+
"author": "BMC-Soft Team",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
56
|
+
"axios": ">=1.0.0",
|
|
57
|
+
"react": ">=18.0.0",
|
|
58
|
+
"react-native": ">=0.72.0",
|
|
59
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
60
|
+
"react-native-webview": ">=13.0.0",
|
|
61
|
+
"lottie-react-native": ">=6.0.0",
|
|
62
|
+
"react-native-svg": ">=14.0.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"axios": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@react-keycloak/keycloak-ts": "^0.2.4",
|
|
71
|
+
"@types/crypto-js": "^4.2.0",
|
|
72
|
+
"crypto-js": "^4.2.0",
|
|
73
|
+
"react-native-get-random-values": "^1.11.0",
|
|
74
|
+
"react-native-keychain": "^8.1.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
78
|
+
"@types/react": "^18.0.0",
|
|
79
|
+
"@types/react-native": "^0.72.0",
|
|
80
|
+
"axios": ">=1.0.0",
|
|
81
|
+
"lottie-react-native": ">=6.0.0",
|
|
82
|
+
"react": ">=18.0.0",
|
|
83
|
+
"react-native": ">=0.72.0",
|
|
84
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
85
|
+
"react-native-svg": ">=14.0.0",
|
|
86
|
+
"react-native-webview": ">=13.0.0",
|
|
87
|
+
"typescript": "^5.3.0"
|
|
88
|
+
},
|
|
89
|
+
"sideEffects": false,
|
|
90
|
+
"repository": {
|
|
91
|
+
"type": "git",
|
|
92
|
+
"url": "https://github.com/waohwaohwaoh/keycloak_auth.git"
|
|
93
|
+
},
|
|
94
|
+
"homepage": "https://github.com/waohwaohwaoh/keycloak_auth#readme",
|
|
95
|
+
"bugs": {
|
|
96
|
+
"url": "https://github.com/waohwaohwaoh/keycloak_auth/issues"
|
|
97
|
+
}
|
|
98
|
+
}
|