@bmc-soft/keycloak-auth
v2.0.19
Published
Production-ready Keycloak authentication package for React Native with optimized performance and security
Downloads
2,042
Readme
@bmc-soft/keycloak-auth
Готовый к продакшену пакет аутентификации Keycloak для React Native.
Установка
npm install @bmc-soft/keycloak-auth
# или
yarn add @bmc-soft/keycloak-authPeer-зависимости
npm install react-native-keychainНастройка нативных модулей: react-native-keychain.
Опционально (для интерцепторов): axios (>=1.0.0).
Для UI нужны: @gorhom/bottom-sheet, react-native-webview, react-native-safe-area-context, lottie-react-native, react-native-svg.
npm install @gorhom/bottom-sheet react-native-webview react-native-safe-area-context lottie-react-native react-native-svgНастройка нативных модулей: @gorhom/bottom-sheet, react-native-webview, react-native-safe-area-context, lottie-react-native, react-native-svg.
Полифиллы для React Native подключаются автоматически при использовании пакета; отдельная настройка keycloak-js не требуется.
Быстрый старт
1. Оборачиваем приложение в KeycloakProvider
import { KeycloakProvider } from '@bmc-soft/keycloak-auth';
const App = () => (
<KeycloakProvider
config={{
url: 'https://your-keycloak-server.com',
realm: 'your-realm',
clientId: 'your-client-id',
}}
redirectUri="myapp://callback"
>
<YourApp />
</KeycloakProvider>
);2. Экран входа
import { AuthPage } from '@bmc-soft/keycloak-auth';
const LoginScreen = () => (
<AuthPage
logo={require('./logo.png')}
onSuccess={(accessToken) => {
// Сохраняем access token в сессию приложения и переходим
Session.events.onLogin(accessToken);
navigation.replace('Home');
}}
onError={(err) => console.error(err)}
/>
);3. (Опционально) Настройка интерцепторов Axios
Если используете axios, задайте провайдер токенов (см. Axios) и вызовите setupAxiosInterceptors с tokenProvider и onReauthRequired. Провайдер обычно настраивается внутри KeycloakProvider после инициализации keycloak (см. Интеграция).
Требования к Keycloak client
Для нового mobile flow настройте Keycloak client как:
- Client type: public
- Flow: Authorization Code / Standard Flow
- PKCE: required,
S256 - Scope: разрешён
offline_access - Redirect URI: только URI приложения, например
myapp://callback
client_secret не передаётся и не хранится в React Native приложении.
Провайдер и конфигурация
KeycloakProvider
| Проп | Тип | Обязательный | Описание |
|------|-----|--------------|----------|
| children | ReactNode | да | Дерево приложения |
| config | KeycloakConfigWith2FA | да | url, realm, clientId; clientId2fa оставлен для legacy-сценариев, новый мобильный flow использует один public client + PKCE |
| redirectUri | string | да | URI OAuth callback (например myapp://callback) |
| theme | KeycloakTheme | нет | Цвета, шрифты, LoaderComponent, компоненты кнопок |
| onTokens | (tokens: KeycloakTokens) => void | нет | Вызывается при изменении токенов (логин, refresh, logout) |
| offlineAccessEnabled | boolean | нет | Запрашивать offline_access в login URL; по умолчанию true |
| backgroundReauth | { enabled?: boolean; thresholdMs?: number; internalNetworkMode?: 'ip-host-is-internal' } | нет | Настройки PIN/биометрии после возврата из background; по умолчанию enabled: true, thresholdMs: 60000 |
| clientSecret | string | нет | Секрет confidential client для legacy-конфигураций; для новых мобильных flows предпочтителен public client + PKCE |
| getClientSecret | () => string | undefined | Promise<string | undefined> | нет | Ленивый резолвер секрета; имеет приоритет над clientSecret |
| initRecoveryStrategy | 'none' \| 'clear-tokens-and-retry' | нет | Что делать, если init со stored tokens упал; по умолчанию none |
| deferStoredTokensUntilRefresh | boolean | нет | Не передавать persisted tokens в keycloak.init() до локального PIN/биометрии; по умолчанию false |
| autoRefreshToken | boolean | нет | По умолчанию true |
| autoRefreshTokenMinValidity | number | нет | За сколько секунд до истечения вызывать refresh; по умолчанию 5 |
| onReauthRequired | () => void | нет | Вызывается при ошибке обновления токена (например показать экран реавторизации) |
| onRefreshLifecycle | (event: KeycloakRefreshLifecycleEvent) => void | нет | Безопасные диагностические события refresh: started, joined, deferred, persisted, succeeded, failed |
deferStoredTokensUntilRefresh полезен для mobile unlock flow: приложение может сначала показать PIN/биометрию, а уже после успешного локального подтверждения выполнить refresh через сохранённый offline token. Это предотвращает преждевременный расход refresh/offline token при старте приложения.
Refresh diagnostics intentionally do not include access, refresh/offline, or ID tokens, authorization headers, cookies, client secrets, PINs, passwords, request bodies, or credentials:
<KeycloakProvider
config={config}
redirectUri={redirectUri}
onRefreshLifecycle={event => logger.info('keycloak-refresh', event)}
>
{children}
</KeycloakProvider>Provider-owned refreshes emit this callback for token expiry and PIN/biometry reauth paths. Independently constructed Axios token providers are configured separately:
const tokenProvider = new KeycloakTokenProvider(keycloakClient, {
onRefreshLifecycle: event => logger.info('keycloak-refresh', event),
});KeycloakTheme
Все поля опциональны; недостающие подставляются из дефолтной темы. Передаётся в KeycloakProvider как theme.
- fonts:
primary,heading - colors:
primary,background,error,text,button,buttonText,link,outlinedButtonBackground,outlinedButtonText,numberPadButtonBackground,numberPadText,numberPadDisabled,pinIndicatorEmpty,border,success - LoaderComponent: компонент состояния загрузки
- ContainedButtonComponent, OutlinedButtonComponent, IconButtonComponent: компоненты кнопок (пропсы см. в THEMING.md)
Полная структура и примеры: THEMING.md.
Компоненты
Экраны
AuthPage
Первый вход: логин в WebView → установка PIN → обмен code на токены. Используется на экране «Вход».
| Проп | Тип | По умолчанию | Описание |
|------|-----|--------------|----------|
| onSuccess | (token: string) => void | — | После успешного обмена code; передаётся access token |
| onError | (error: Error) => void | — | При ошибке обмена или сохранения |
| logo | ReactNode | ImageSourcePropType | — | Логотип приложения |
| logoHeight, logoWidth | number | 80 | Размер логотипа при использовании image source |
| showBiometryPrompt | boolean | true | Спрашивать биометрию после установки PIN |
| pinLength | number | 4 | Длина PIN |
| style, paddingTop, paddingBottom | ViewStyle / number | — | Стили контейнера |
ConfirmAuthPage
Подтверждение сессии: сначала PIN/биометрия, затем продолжение с текущим access token или refresh через offline token. Если repeat-login не может восстановить токены через offline token, открывается обычный Keycloak login. Скрытый ввод username/password в новом flow не используется.
Режимы:
login— повторный вход в приложение; при невозможности refresh открывает обычный Keycloak login.unlock— локальная разблокировка после background; refresh выполняется только если access token уже истёк.reauth— повторная авторизация после 401; refresh через offline token обязателен.
| Проп | Тип | По умолчанию | Описание |
|------|-----|--------------|----------|
| onSuccess | () => void | — | Успешное локальное подтверждение и восстановление/проверка access token |
| onError | (error: Error) => void | — | Например неверный PIN |
| onLogout | () => void | — | После очистки хранилища пакета; в приложении — очистить сессию и перейти на экран входа |
| mode | 'login' \| 'unlock' \| 'reauth' | "login" | Сценарий: повторный вход, unlock после background или reauth после 401 |
| logoutText | string | "Выйти из аккаунта" | Текст ссылки «Выйти» |
| logo, logoHeight, logoWidth | — | 80 | Логотип |
| pinLength | number | 4 | Длина PIN |
| allowBiometry, autoShowBiometry | boolean | true | Биометрия |
| title, description | string | — | Заголовок и описание блока PIN |
| layout | 'screen' \| 'sheet' | "screen" | Компактная раскладка sheet для встраивания в BottomSheet |
| style | ViewStyle | — | Стиль контейнера |
Виджеты
ReauthBottomSheet
BottomSheet с подтверждением PIN для реавторизации. Отображение (открытие/закрытие) определяется внутри библиотеки по состоянию реавторизации (isReauthRequired из ReauthContext) — передавать видимость снаружи не нужно. Остальное управление — через пропсы и коллбэки (не привязан к Session приложения).
| Проп | Тип | По умолчанию | Описание |
|------|-----|--------------|----------|
| onSuccess | () => void | — | Реавторизация прошла успешно |
| onDismiss | () => void | — | Закрытие без успеха |
| onError | (error: Error) => void | — | Ошибка ввода PIN |
| pinLength | number | 4 | Длина PIN |
| allowBiometry, autoShowBiometry | boolean | true | Биометрия |
| mode | 'login' \| 'unlock' \| 'reauth' | авто | Если не передан, background открывается как unlock, остальные причины как reauth |
| title | string | "Введите PIN" | Заголовок |
| description | string | "Введите PIN-код" | Описание |
Кнопки выхода
LogoutButtonText
Текстовая кнопка (contained или outlined). По нажатию открывается полноэкранный Modal с WebViewLogout. Должна использоваться внутри KeycloakProvider.
- Базовые:
onLogoutSuccess,onError,style - Специфичные:
variant: 'contained' | 'outlined',label: string
LogoutButtonIcon
Кнопка выхода только с иконкой. Базовые пропсы плюс renderIcon: ReactNode.
Остальные UI (внутри пакета или для кастомных сценариев)
- WebViewLogin, WebViewLogout — OAuth-потоки в WebView
- PINSetup, PINConfirm — ввод и подтверждение PIN
- NumberPad, PINIndicator — UI ввода PIN
- LogoutConfirmSheet — подтверждение выхода
Хуки
Все хуки должны вызываться внутри KeycloakProvider (если не указано иное).
useKeycloakAuth()
Полный API авторизации: инстанс keycloak, токен, login, logout, URL, состояние реавторизации.
Возвращает: keycloak, isInitialized, isLoading, error, accessToken, token, offlineToken, refreshToken, isExpired, isAuthenticated, updateToken, login, logout, loadUserProfile, createLoginUrl, createLogoutUrl, clearTokens, isReauthRequired, showReauth, hideReauth.
useToken()
Доступ только к токенам (меньше ре-рендеров). Возвращает: accessToken, token, offlineToken, refreshToken, idToken, isExpired, updateToken, clearTokens.
useReauth()
Состояние UI реавторизации. Возвращает: isReauthRequired, reauthReason, showReauth, hideReauth.
Состояние isReauthRequired:
- Флаг
isReauthRequiredизuseKeycloakAuth()/useReauth()выставляется вtrueавтоматически при любой ошибке обновления токена: из Keycloak (onTokenExpired, возврат в приложение с истёкшим токеном) и из axios interceptor (ошибка refresh или превышение лимита retry). - Флаг сбрасывается автоматически при успешном завершении реавторизации в компонентах пакета (ReauthBottomSheet, ConfirmAuthPage) и при успешном refresh в axios interceptor. Вызывать
hideReauth()в колбэкеonSuccessпри использовании этих компонентов не обязательно — достаточно своей логики (навигация, Session.events, закрытие sheet). - Если приложение реализует собственный экран реавторизации (без ReauthBottomSheet/ConfirmAuthPage пакета), после успешной реавторизации нужно вызвать
hideReauth().
useKeycloakAuthScreen(options?)
Определяет, какой экран авторизации показывать: 'login' или 'confirm'. Используется для initialRouteName в Auth Stack.
Возвращает: screen: 'login' | 'confirm' | null, isLoading, error.
Опции: shouldShowConfirm?: () => Promise<boolean> — переопределить стандартную проверку (наличие offline token и PIN).
useKeycloakTheme()
Текущая тема (объединённая с дефолтной). Возвращает: fonts, colors, LoaderComponent, ContainedButtonComponent, OutlinedButtonComponent, IconButtonComponent. Можно вызывать вне провайдера (вернётся дефолтная тема).
Хранилище
tokenStorage
Хранение токенов в Keychain. API: getToken/getAccessToken, saveToken/saveAccessToken, getRefreshToken/getOfflineToken, saveRefreshToken/saveOfflineToken, getIdToken, saveIdToken, getTokens, saveTokens, clearTokens, hasTokens.
refresh_token из Keycloak при offline_access хранится как offline token. Он не отправляется в backend; backend получает только Authorization: Bearer <accessToken>. После успешного ConfirmAuthPage вызовите tokenStorage.getAccessToken(), чтобы получить текущий access token и обновить сессию приложения.
Важно: не храните токены в AsyncStorage. Используйте только Keychain через пакет.
credentialStorage
Используется внутри пакета для PIN-verifier и биометрии. Legacy-хранение зашифрованных credentials оставлено для совместимости, но новый offline_access flow не использует username/password для повторного входа.
Axios
Интерфейс TokenProvider
Используется интерцепторами для получения и обновления токенов:
getToken(): Promise<string | null> | string | nullrefreshToken(): Promise<string | null>hasRefreshToken?(): Promise<boolean> | boolean(опционально)formatToken?(token: string): string(опционально; по умолчаниюBearer ${token})
setupAxiosInterceptors(axiosInstance, config)
Добавляет интерцепторы: в запрос — подстановка токена; в ответ — retry при 401, обновление токена, при ошибке — вызов onReauthRequired.
config: tokenProvider, onReauthRequired, onTokenRefreshed, onRefreshError, maxRetries (по умолчанию 1), autoAddToken (true), autoRetryOn401 (true), excludeEndpoints.
Возвращает { cleanup } для снятия интерцепторов.
KeycloakTokenProvider
Класс, реализующий TokenProvider на основе инстанса KeycloakReactNativeClient. Удобно, когда инстанс клиента уже есть и нужен адаптер для интерцепторов.
Интеграция
Пошаговая интеграция с примерами кода. Рассматривается только Keycloak.
1. Оборачивание приложения в KeycloakProvider
Конфиг и redirectUri берут из настроек приложения. Тему (Loader, кнопки, цвета) передайте из темы приложения. В onTokens сохраняйте access token в хранилище сессии; в onReauthRequired показывайте экран реавторизации.
Порядок провайдеров: если используется BottomSheetModalProvider из @gorhom/bottom-sheet (например для реавторизации или других модалок), он должен находиться внутри KeycloakProvider. Иначе ReauthBottomSheet или ConfirmAuthPage внутри модалки не получат контекст Keycloak. Порядок: KeycloakProvider → BottomSheetModalProvider → остальное дерево приложения.
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { KeycloakProvider, type KeycloakTheme } from '@bmc-soft/keycloak-auth';
const theme: KeycloakTheme = {
colors: {
primary: appColors.primary,
background: appColors.background,
error: appColors.error,
text: appColors.text,
numberPadButtonBackground: appColors.cardBackground,
numberPadText: appColors.text,
pinIndicatorEmpty: appColors.textMuted,
},
LoaderComponent: AppLoader,
ContainedButtonComponent: AppContainedButton,
OutlinedButtonComponent: AppOutlinedButton,
IconButtonComponent: AppIconButton,
};
export const App = () => (
<KeycloakProvider
config={{
url: 'https://sso.example.com',
realm: 'my-realm',
clientId: 'my-app',
}}
redirectUri="myapp://callback"
theme={theme}
onTokens={({ accessToken }) => {
Session.events.onChangeAuthToken(accessToken);
}}
onReauthRequired={() => {
Session.events.onRequireReauth();
}}
>
<BottomSheetModalProvider>
<RootNavigator />
</BottomSheetModalProvider>
</KeycloakProvider>
);2. Установка TokenProvider для axios
Провайдер токенов настраивается один раз внутри KeycloakProvider после инициализации keycloak. Он отдаёт backend только access token, а refresh выполняет через offline token.
import {
KeycloakTokenProvider,
setupAxiosInterceptors,
useKeycloakAuth,
} from '@bmc-soft/keycloak-auth';
const api = axios.create({ baseURL: 'https://api.example.com' });
const KeycloakAxiosTokenProviderSetup = ({ children }) => {
const { keycloak, isInitialized } = useKeycloakAuth();
useEffect(() => {
if (!isInitialized || !keycloak) return undefined;
const { cleanup } = setupAxiosInterceptors(api, {
tokenProvider: new KeycloakTokenProvider(keycloak),
onReauthRequired: () => {
Session.events.onRequireReauth();
},
onTokenRefreshed: (accessToken) => {
Session.events.onChangeAuthToken(accessToken);
},
});
return cleanup;
}, [isInitialized, keycloak]);
return <>{children}</>;
};
// Внутри KeycloakProvider:
<KeycloakProvider config={...} redirectUri={...} ...>
<KeycloakAxiosTokenProviderSetup>
<BottomSheetModalProvider>
<RootNavigator />
</BottomSheetModalProvider>
</KeycloakAxiosTokenProviderSetup>
</KeycloakProvider>Если у приложения уже есть свой tokenProvider, его методы должны соблюдать тот же контракт:
setupAxiosInterceptors(api, {
tokenProvider: {
getToken: () => keycloak.token || null,
refreshToken: async () => {
await keycloak.updateToken(-1);
return keycloak.token || null;
},
hasRefreshToken: () => Boolean(keycloak.refreshToken),
},
onReauthRequired: () => {
Session.events.onRequireReauth();
},
});3. Стек авторизации (Login + Confirm)
Два экрана: Login (AuthPage) и Confirm (ConfirmAuthPage). Начальный маршрут задаётся через useKeycloakAuthScreen(): показывать Confirm, если есть offline token и PIN, иначе Login.
import { useKeycloakAuthScreen, AuthPage, ConfirmAuthPage, tokenStorage } from '@bmc-soft/keycloak-auth';
const Stack = createNativeStackNavigator();
export const AuthStack = () => {
const { screen, isLoading } = useKeycloakAuthScreen();
if (isLoading) return <Loader />;
const initialRoute = screen === 'confirm' ? 'Confirm' : 'Login';
return (
<Stack.Navigator initialRouteName={initialRoute}>
<Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
<Stack.Screen name="Confirm" component={ConfirmScreen} options={{ headerShown: false }} />
</Stack.Navigator>
);
};
const LoginScreen = () => (
<AuthPage
logo={require('./logo.png')}
onSuccess={(accessToken) => Session.events.onLogin(accessToken)}
onError={(err) => console.error(err)}
/>
);
const ConfirmScreen = () => {
const navigation = useNavigation();
const onSuccess = useCallback(async () => {
const accessToken = await tokenStorage.getAccessToken();
if (accessToken) Session.events.onLogin(accessToken);
}, []);
return (
<ConfirmAuthPage
onSuccess={onSuccess}
onLogout={() => {
Session.events.onLogout();
navigation.replace('Login');
}}
pinLength={4}
/>
);
};4. Реавторизация при 401
При 401 интерцепторы вызывают onReauthRequired. Можно использовать isReauthRequired из useKeycloakAuth() как единственный источник правды для отображения реавторизации. Если используете готовый ReauthBottomSheet из пакета, он сам открывается и закрывается по isReauthRequired — достаточно смонтировать компонент и при необходимости передать коллбэки (onSuccess, onDismiss). Для кастомной обёртки (свой BottomSheet и контент) можно использовать ConfirmAuthPage и вручную управлять видимостью по isReauthRequired. После успешного ввода PIN получите access token из пакета, обновите сессию и закройте реавторизацию. Флаг isReauthRequired сбрасывается пакетом автоматически при успехе в ReauthBottomSheet/ConfirmAuthPage; в onSuccess достаточно своей логики (обновление сессии, закрытие sheet).
Пример с кастомным BottomSheet и ConfirmAuthPage (управление видимостью вручную):
import { ConfirmAuthPage, tokenStorage } from '@bmc-soft/keycloak-auth';
import BottomSheet from '@gorhom/bottom-sheet';
export const CustomReauthSheet = () => {
const sheetRef = useRef(null);
const showReauth = useStore($reauthRequired); // например Effector / useState
useEffect(() => {
showReauth ? sheetRef.current?.snapToIndex(0) : sheetRef.current?.close();
}, [showReauth]);
const handleSuccess = useCallback(async () => {
const accessToken = await tokenStorage.getAccessToken();
if (accessToken) {
Session.events.onLogin(accessToken);
Session.events.onReauthCompleted();
}
sheetRef.current?.close();
}, []);
return (
<BottomSheet ref={sheetRef} snapPoints={['50%']} enablePanDownToClose>
<ConfirmAuthPage mode="reauth" onSuccess={handleSuccess} pinLength={4} />
</BottomSheet>
);
};5. PIN/биометрия после возврата из background
Если backgroundReauth.enabled !== false, библиотека отслеживает AppState: при возврате из background/inactive через более чем thresholdMs миллисекунд открывается ReauthBottomSheet.
По умолчанию:
<KeycloakProvider
config={{ url: 'https://sso.example.com', realm: 'my-realm', clientId: 'my-app' }}
redirectUri="myapp://callback"
backgroundReauth={{
enabled: true,
thresholdMs: 60000,
internalNetworkMode: 'ip-host-is-internal',
}}
>
<App />
</KeycloakProvider>Правило внутренней сети: если host в config.url является IP-адресом, сеть считается внутренней и background sheet не показывается; если host является DNS-именем, сеть считается внешней.
6. Выход
Используйте LogoutButtonIcon или LogoutButtonText. По нажатию открывается Modal с WebViewLogout; при успешном выходе вызывается onLogoutSuccess — там очищайте сессию приложения. WebView logout проходит через страницу Keycloak, а затем библиотека отзывает текущий offline token (refresh_token) через token revocation endpoint до локальной очистки токенов.
import { LogoutButtonIcon, LogoutButtonText } from '@bmc-soft/keycloak-auth';
export const LogoutButton = ({ variant }: { variant: 'icon' | 'text' }) => {
const handleLogoutSuccess = useCallback(() => {
Session.events.onLogout();
}, []);
if (variant === 'icon') {
return (
<LogoutButtonIcon
renderIcon={<Icon name="logout" color={theme.error} />}
onLogoutSuccess={handleLogoutSuccess}
/>
);
}
return (
<LogoutButtonText
variant="outlined"
label="Выйти"
onLogoutSuccess={handleLogoutSuccess}
/>
);
};Схема потока
KeycloakProvider → инициализация Keycloak → установка TokenProvider для axios
↓
useKeycloakAuthScreen → Login (AuthPage) или Confirm (ConfirmAuthPage)
↓
Confirm → PIN/биометрия → текущий access token или refresh через offline token
↓
onSuccess → Session.onLogin(accessToken) | onLogout → Session.onLogout + навигация
↓
axios 401 / background > 60с вне внутренней сети → ReauthBottomSheet с ConfirmAuthPage
↓
PIN/биометрия → refresh при необходимости → tokenStorage.getAccessToken() → закрыть sheetАрхитектура и безопасность
Mobile client и offline_access
Для мобильного приложения используйте Keycloak public client + Authorization Code Flow with PKCE (S256). client_secret не должен попадать в React Native приложение; confidential client допустим только через backend/BFF.
При offlineAccessEnabled=true библиотека добавляет offline_access в login scope. Keycloak возвращает offline token в поле refresh_token; библиотека хранит его в Keychain как offlineToken/refreshToken и использует только для получения новых access token.
Refresh token rotation
Библиотека поддерживает строгую ротацию refresh token в Keycloak, включая настройки Revoke Refresh Token = ON и Refresh Token Max Reuse = 0.
Все внутренние пути обновления токена (autoRefreshToken, useToken().updateToken, KeycloakTokenProvider.refreshToken) проходят через общий single-flight guard на один Keycloak client. Совместимые параллельные refresh-запросы ждут один и тот же результат, а запрос с большим minValidity дожидается текущего refresh и затем проверяет уже актуальную пару токенов. После успешного refresh новая пара accessToken/offlineToken сохраняется в Keychain до уведомления consumers.
Интеграционному приложению не нужно запускать дополнительные параллельные keycloak.updateToken() поверх библиотеки. Если нужен ручной refresh, используйте useToken().updateToken() или KeycloakTokenProvider.refreshToken().
Ошибки обновления токена
Если token endpoint Keycloak возвращает ошибку при exchange или refresh, библиотека выбрасывает KeycloakTokenError. Это обычный Error, поэтому существующий код может продолжать читать error.message, но дополнительно доступны структурированные поля:
type KeycloakTokenOperation = 'exchange' | 'refresh' | 'revoke';
type KeycloakTokenError = Error & {
name: 'KeycloakTokenError';
operation: KeycloakTokenOperation;
status?: number;
error?: string;
errorDescription?: string;
errorUri?: string;
details: {
operation: KeycloakTokenOperation;
status?: number;
error?: string;
errorDescription?: string;
errorUri?: string;
};
};Для refresh-token ошибок публичный контракт можно сузить до KeycloakRefreshError:
type KeycloakRefreshError = Error & {
status?: number;
error?: string;
errorDescription?: string;
operation: 'refresh';
};Поля error, errorDescription и errorUri соответствуют OAuth token endpoint response (error, error_description, error_uri), а status содержит HTTP status ответа. Например, invalid_grant, Offline user session not found, revoked/expired offline token и HTTP 400/401 различимы по структурированным полям без парсинга message. Network errors тоже нормализуются с operation: 'refresh', но без status и без OAuth error, поэтому приложение не должно считать их истёкшей сессией. Токены, request body, cookies, authorization headers и client secret в объект ошибки не добавляются.
useToken().updateToken(), useKeycloakAuth().updateToken(), ConfirmAuthPage.onError, ReauthBottomSheet.onError и axios onRefreshError получают тот же объект ошибки. KeycloakTokenProvider.refreshToken() не преобразует refresh failure в null: ошибка пробрасывается дальше, чтобы onRefreshError мог получить исходные детали.
При logout библиотека сначала выполняет браузерный WebView logout flow, затем отправляет текущий offline token (refresh_token) в Keycloak token revocation endpoint (/protocol/openid-connect/revoke) с token_type_hint=refresh_token и только после этого очищает локальные токены. Это важно для offline_access: обычный browser logout не обязан удалять offline-сессию в Keycloak.
Иерархия контекстов
KeycloakConfigProvider ← конфиг (редко меняется)
└─ KeycloakInstanceProvider ← инстанс (один раз)
└─ KeycloakThemeProvider ← тема (опционально; мемоизирована)
└─ TokenProvider ← токены (частые обновления)
└─ ReauthProvider ← состояние реавторизацииОбновление токенов не вызывает ре-рендер компонентов, зависящих только от конфига, инстанса или темы.
Безопасность
- Токены в OS Keychain (не AsyncStorage)
- PIN хранится как verifier в Keychain; legacy-хранилище encrypted credentials сохранено только для обратной совместимости
- Offline token не отправляется в backend и используется только для refresh в Keycloak
- Запросы по HTTPS
- Username/password не используются для repeat-login в новом offline_access flow
Экспорты
- Основной:
@bmc-soft/keycloak-auth— провайдер, хуки, экраны, виджеты, UI, хранилище, помощники axios, типы - Подпути:
@bmc-soft/keycloak-auth/screens,/widgets,/hooks,/axios,/context,/storage(см.exportsв package.json)
Лицензия
MIT © BMC-Soft Team
