npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bmc-soft/keycloak-auth

v2.0.25

Published

Production-ready Keycloak authentication package for React Native with optimized performance and security

Readme

@bmc-soft/keycloak-auth

Готовый к продакшену пакет аутентификации Keycloak для React Native.

Установка

npm install @bmc-soft/keycloak-auth
# или
yarn add @bmc-soft/keycloak-auth

Canonical 2.0.25 release builds are packed as bmc-soft-keycloak-auth-2.0.25.tgz. Historical QA tarballs with suffixes such as refresh-admission or reauth-biometry are preserved for traceability, but they are not the canonical release artifact.

Known security limitation. Legacy security limitation: WebViewLogin can bridge plaintext username/password from the WebView into React Native state and the public onLoginSuccess callback. This behavior predates the offline-access refresh flow and is retained only for compatibility with existing hosts. New integrations should not consume the credential-bearing callback fields. Prefer an authorization-code/PKCE or token-only host flow, and treat the callback url as the only field needed for the library's code/token exchange.

Peer-зависимости

Сам пакет устанавливает свои обычные зависимости сам, включая @react-keycloak/keycloak-ts, @types/crypto-js, crypto-js, react-native-get-random-values и react-native-keychain. Устанавливать react-native-keychain отдельной командой не нужно.

Требования к peer dependencies из package.json:

| Пакет | Диапазон | Когда нужен | |-------|----------|-------------| | react | >=18.0.0 | обязательный peer; используйте уже установленную совместимую версию host-приложения | | react-native | >=0.72.0 | обязательный peer; используйте уже установленную совместимую версию host-приложения | | @gorhom/bottom-sheet | >=5.0.0 | LogoutConfirmSheet и host-owned/custom bottom-sheet surfaces; built-in ReauthBottomSheet его не использует | | lottie-react-native | >=6.0.0 | UI success-анимации | | react-native-reanimated | >=2.8.0 <5 | UI; выберите версию, совместимую с RN baseline приложения | | react-native-safe-area-context | >=4.0.0 | UI экранов и виджетов | | react-native-svg | >=14.0.0 | UI и иконки | | react-native-webview | >=13.0.0 | Keycloak login/logout WebView | | @react-native-async-storage/async-storage | >=1.17.0 | только legacy/fresh-install cleanup; токены в нём не хранятся | | axios | >=1.0.0, optional | только экспорт /axios |

Сохраните уже установленные react и react-native, совместимые с вашим приложением. Для каждого отсутствующего UI/support peer выберите версию из указанного диапазона, которую поддерживает RN baseline host-приложения; не используйте незафиксированные latest-версии native modules, способные тихо изменить эту совместимость.

При необходимости отдельно добавьте подходящий для baseline вариант react-native-reanimated; не используйте для core-пакетов или Reanimated незафиксированную установку, которая может изменить совместимость приложения. axios (>= 1.0.0) — единственный optional peer: устанавливайте его только если используете экспорт /axios.

npm install axios # только для setupAxiosInterceptors / KeycloakTokenProvider

@react-native-async-storage/async-storage используется пакетом только для legacy/fresh-install cleanup; access, refresh/offline и ID tokens остаются в OS Keychain. Не переносите токены в AsyncStorage.

@gorhom/bottom-sheet требует react-native-reanimated и react-native-gesture-handler; последний намеренно не объявлен peer dependency этого пакета, потому что нужен только при использовании Gorhom bottom-sheet surfaces (LogoutConfirmSheet или host-owned/custom sheets). Built-in ReauthBottomSheet — обычный blocking overlay на React Native View, поэтому для него не нужен BottomSheetModalProvider. Настройте Gorhom по официальной инструкции Bottom Sheet (в частности, GestureHandlerRootView для Gesture Handler v2) и завершите настройку Reanimated, если используете такие surfaces.

Нативная настройка

После установки нативных пакетов для iOS выполните npx pod-install (или cd ios && pod install), затем пересоберите приложение. Для Face ID добавьте в ios/<приложение>/Info.plist понятное пользователю значение NSFaceIDUsageDescription, например:

<key>NSFaceIDUsageDescription</key>
<string>Face ID используется для подтверждения входа.</string>

См. также инструкции react-native-keychain, react-native-webview, safe-area-context, Lottie и react-native-svg. React Native автоматически связывает поддерживаемые native modules, но pods и новая сборка всё равно обязательны для iOS.

Полифиллы для 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 | | initOnLoad | RNKeycloakInitOptions['onLoad'] | нет | Поведение Keycloak при init; если не передан, явный login выполняют AuthPage/WebViewLogin | | autoRefreshToken | boolean | нет | По умолчанию true | | autoRefreshTokenMinValidity | number | нет | За сколько секунд до истечения вызывать refresh; по умолчанию 5 | | onReauthRequired | () => void | нет | Provider-сигнал локальной реавторизации: background PIN gate или подтверждённая terminal ошибка auto-refresh; не вызывается для каждой refresh-ошибки | | onInteractiveRecoveryRequired | (error: KeycloakRefreshOutcomeUnknownError) => void | нет | Provider-сигнал, что результат refresh неизвестен и приложение должно выбрать интерактивное восстановление | | onRefreshLifecycle | (event: KeycloakRefreshLifecycleEvent) => void | нет | Безопасные диагностические события refresh: started, joined, deferred, persisted, succeeded, failed | | webViewLoginInjectedJavaScript | string | нет | JavaScript, внедряемый в login WebView до загрузки страницы | | webViewLoginPageTheme | 'base' | нет | Встроенная тема страницы входа; base для Keycloak base template |

deferStoredTokensUntilRefresh полезен для mobile unlock flow: приложение может сначала показать PIN/биометрию, а уже после успешного локального подтверждения выполнить refresh через сохранённый offline token. Это предотвращает преждевременный расход refresh/offline token при старте приложения.

При смене уведомительных onSessionTerminated и onInteractiveRecoveryRequired действующий клиент и его токены сохраняются; используются последние обработчики. Фактическая смена конфигурации или init-параметров пересоздаёт клиент. Сохраняйте стабильную identity getClientSecret и refreshAdmission, когда их поведение не меняется. Не загружайте токены вручную до прохождения локального PIN/биометрии.

Refresh diagnostics intentionally do not include access, refresh/offline, or ID tokens, authorization headers, cookies, client secrets, PINs, passwords, request bodies, or credentials. Lifecycle telemetry is sanitized: refresh failure events may include safe fields such as status, classification, diagnosticCode, stage, operation, and outcomeUnknown, but OAuth error and errorDescription are not lifecycle event fields:

<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),
});

Refresh admission, shared flight и recovery

Если передан refreshAdmission, refresh допускается только после стабильной проверки exact Keycloak token origin. Безопасные defaults библиотеки: два успешных probe, интервал 1000 мс, timeout probe 2000 мс, reassessment 1500 мс и общий deadline 10000 мс; probe method is GET. Тип соединения и VPN metadata используются только как advisory diagnostics и сами по себе не разрешают и не запрещают refresh. No corporate VPN is required by the library.

Граница физического запроса явная: admission и journal находятся в PRE_REQUEST, а REQUEST_DISPATCHED начинается синхронно при вызове token POST. Последний caller может отменить pre-request работу; после dispatch caller только отсоединяется, физический запрос не abort/retry. Same-client и cross-client вызовы присоединяются к одному replayable refresh flight через beginTokenUpdate(), а обычный updateToken() остаётся Promise-совместимой обёрткой.

maxAmbiguousReuseAttempts is clamped to the integer range 0..3; non-finite or non-number values become 0, and fractional values are truncated. Durable refresh-attempt journal records use the states IN_FLIGHT, QUARANTINED, and SESSION_INVALID; the public stored-token dispositions derived from them remain usable, outcome-unknown, and session-invalid.

Durable disposition сохранённого refresh token имеет значения usable, outcome-unknown и session-invalid. Unsafe token не hydrate/retry. Outcome unknown и invalid_grant открывают locked fresh-login recovery, а invalid_client/unauthorized_client — отдельный configuration recovery. Transient/deferred ошибки эти состояния не создают. Публичный useKeycloakInteractiveRecovery() предоставляет startFreshLogin(action) и coalesced explicitLogout(action?); fresh login завершает recovery только после durable persistence всех новых token components.

KeycloakTheme

Все поля опциональны; недостающие подставляются из дефолтной темы. Передаётся в KeycloakProvider как theme.

  • fonts: primary, heading
  • colors: primary, background, error, text, button, buttonText, link, outlinedButtonBackground, outlinedButtonText, numberPadButtonBackground, numberPadText, numberPadDisabled, pinIndicatorEmpty, pinIndicatorErrorEmpty, 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 | — | При ошибке обмена или сохранения | | onStageChange | (stage: 'LOGIN' \| 'SETUP_PIN' \| 'PROCESSING' \| 'SUCCESS') => void | — | Смена стадии авторизации | | logo | ReactNode | ImageSourcePropType | — | Логотип приложения | | logoHeight, logoWidth | number | 80 | Размер логотипа при использовании image source | | showBiometryPrompt | boolean | true | Спрашивать биометрию после установки PIN | | pinLength | number | 4 | Длина PIN | | style, paddingTop, paddingBottom | ViewStyle / number | — | Стили контейнера | | successFeedback | SuccessFeedbackConfig | см. ниже | Короткое success-состояние перед финальным onSuccess |

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 | — | Ошибка outer flow, не неверный PIN: WebView login или refresh в unlock/reauth, если terminal ошибка не направлена в onSessionExpired | | onSessionExpired | (error: Error) => void | — | В unlock/reauth: сохранённая Keycloak-сессия подтверждённо истекла или отозвана; показать сообщение и направить на новый login | | onLogout | () => void | — | После очистки хранилища пакета; в приложении — очистить сессию и перейти на экран входа | | mode | 'login' \| 'unlock' \| 'reauth' | "login" | Сценарий: повторный вход, unlock после background или reauth после 401 | | refreshUnavailableContent | RefreshUnavailableContent | English fallback | Локализованные title/message/recommendations и подписи Retry/Logout для admission timeout | | interactiveRecoveryContent | InteractiveRecoveryContent | English fallback | Локализованные тексты fresh-login и configuration recovery; OAuth code остаётся отдельным structured полем | | logoutText | string | "Выйти из аккаунта" | Текст ссылки «Выйти» | | logo | ReactNode | ImageSourcePropType | — | Логотип | | logoHeight | number | 80 | Высота image-source логотипа | | logoWidth | number | 80 | Ширина image-source логотипа | | pinLength | number | 4 | Длина PIN | | allowBiometry, autoShowBiometry | boolean | true | Биометрия | | title | string | "Введите PIN" | Заголовок PIN; effective default задаёт PINConfirm | | description | string | отсутствует (undefined) | Необязательное описание PIN; по умолчанию не отображается | | layout | 'screen' \| 'sheet' | "screen" | Компактная раскладка sheet для встраивания в BottomSheet | | style | ViewStyle | — | Стиль контейнера | | successFeedback | SuccessFeedbackConfig | см. ниже | Короткое success-состояние перед финальным onSuccess |

Неверный PIN (и ошибка его проверки) остаётся внутри PINConfirm: outer onError не вызывается. После локальной проверки PIN access token используется без сети только когда он действителен более 60 секунд; точная граница 60 секунд входит в admission (minValidity = 61). При inactive/background in-memory PIN grant немедленно инвалидируется, а stale lifecycle/result не может закрыть новый экран. Outcome-unknown/session-invalid остаются в locked fresh-login recovery; configuration errors показываются отдельно. Явный Logout даёт remote revoke один общий budget 5000 мс (включая чтение token), затем всегда выполняет local library cleanup перед host onLogout.

Виджеты

ReauthBottomSheet

Blocking full-screen overlay с подтверждением PIN для реавторизации. Это не Gorhom BottomSheet и не BottomSheetModal: отображение (открытие/закрытие) определяется внутри библиотеки по состоянию реавторизации (isReauthRequired из ReauthContext) — передавать видимость снаружи не нужно и BottomSheetModalProvider для встроенного ReauthBottomSheet не требуется. Остальное управление — через пропсы и коллбэки (не привязан к Session приложения).

| Проп | Тип | По умолчанию | Описание | |------|-----|--------------|----------| | onSuccess | () => void | — | Реавторизация прошла успешно | | onDismiss | () => void | — | Закрытие без успеха | | onError | (error: Error) => void | — | Ошибка outer Confirm flow, не неверный PIN: см. ConfirmAuthPage.onError; передаётся его вложенному экрану | | onSessionExpired | (error: Error) => void | — | Сохранённая Keycloak-сессия подтверждённо истекла/отозвана; sheet закроется, приложение выбирает новый login | | onLogout | () => void | — | Пользователь явно выбрал выход из UI реавторизации; библиотека закроет sheet | | pinLength | number | 4 | Длина PIN | | allowBiometry, autoShowBiometry | boolean | true | Биометрия | | mode | 'login' \| 'unlock' \| 'reauth' | авто | Если не передан, background открывается как unlock, остальные причины как reauth | | title | string | "Введите PIN" | Заголовок | | description | string | отсутствует (undefined) | Необязательное описание; по умолчанию не отображается | | refreshUnavailableContent | RefreshUnavailableContent | English fallback | Передаётся в ConfirmAuthPage для locked unavailable UI | | interactiveRecoveryContent | InteractiveRecoveryContent | English fallback | Передаётся в ConfirmAuthPage для fresh-login/configuration recovery | | successFeedback | SuccessFeedbackConfig | см. ниже | Короткое success-состояние перед финальным onSuccess |

ReauthBottomSheet передаёт эти callback-контракты вложенному ConfirmAuthPage: неверный PIN не вызывает его onError; mode-dependent refresh routing остаётся тем же (login → обычный login fallback, terminal unlock/reauth → onSessionExpired, если он задан, иначе onError).

SuccessFeedbackConfig

Общий для AuthPage, ConfirmAuthPage и ReauthBottomSheet проп. По умолчанию success-состояние включено, показывает текст "Успешный вход" и держится 650 мс, затем вызывается финальный onSuccess.

| Поле | Тип | По умолчанию | Описание | |------|-----|--------------|----------| | enabled | boolean | true | Показать success-состояние; при false onSuccess вызывается сразу | | title | string | "Успешный вход" | Текст под анимацией | | durationMs | number | 650 | Длительность до финального onSuccess |

Кнопки выхода

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 выставляется для background PIN gate и Provider-owned подтверждённого terminal refresh failure. Axios выставляет его при исчерпании retry budget (maxRetries); structured refresh errors из Axios идут в shared interactive recovery и onRefreshError, но не вызывают Axios onReauthRequired напрямую.
  • Флаг сбрасывается автоматически при успешном завершении реавторизации в компонентах пакета (ReauthBottomSheet, ConfirmAuthPage). Axios скрывает только уже открытый refresh-related флаг после успешного refresh. Вызывать hideReauth() в onSuccess готовых компонентов обычно не требуется.
  • Если приложение реализует собственный экран реавторизации (без ReauthBottomSheet/ConfirmAuthPage пакета), после успешной реавторизации нужно вызвать hideReauth().

useKeycloakAuthScreen(options?)

Определяет, какой экран авторизации показывать: 'resolving', 'login' или 'confirm'. Пока provider не инициализирован или выполняется проверка Keychain, возвращается 'resolving'; не используйте его как имя маршрута.

Возвращает: screen: 'resolving' | '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 | null
  • refreshToken(): Promise<string | null>
  • clearSession?(): Promise<void> | void (опционально; очистка локальной auth-сессии после подтверждённой terminal refresh-ошибки)
  • hasRefreshToken?(): Promise<boolean> | boolean (опционально)
  • formatToken?(token: string): string (опционально; по умолчанию Bearer ${token})

setupAxiosInterceptors(axiosInstance, config)

Добавляет интерцепторы: в запрос — подстановка токена; в ответ — retry при 401 и refresh. onRefreshError вызывается для любой ошибки refresh. onReauthRequired вызывается только после подтверждённой terminal ошибки refresh или после исчерпания maxRetries; это callback Axios-конфигурации, не Provider prop.

config: tokenProvider, onReauthRequired, onTokenRefreshed, onRefreshError, onSessionTerminated, maxRetries (по умолчанию 1), autoAddToken (true), autoRetryOn401 (true), excludeEndpoints.

При refresh-ошибке Axios очищает очередь ожидающих 401, отдаёт ошибку в shared interactive recovery controller, вызывает onRefreshError(error) и отклоняет исходный запрос этой refresh- ошибкой. Axios known limitation: onSessionTerminated is invoked only for invalid_grant in the current runtime path. Other structured terminal/configuration errors are handed to shared interactive recovery and onRefreshError, but Axios does not own an additional cleanup or reauth callback for them. tokenProvider.clearSession() и fallback tokenStorage.clearTokens() не вызываются из Axios interceptor. Independently constructed Axios token providers also do not receive Provider-owned onRefreshLifecycle unless configured on KeycloakTokenProvider.

Возвращает { cleanup } для снятия интерцепторов.

KeycloakTokenProvider

Класс, реализующий TokenProvider на основе инстанса KeycloakReactNativeClient. Удобно, когда инстанс клиента уже есть и нужен адаптер для интерцепторов.


Интеграция

Пошаговая интеграция с примерами кода. Рассматривается только Keycloak.

1. Оборачивание приложения в KeycloakProvider

Конфиг и redirectUri берут из настроек приложения. Тему (Loader, кнопки, цвета) передайте из темы приложения. В onTokens сохраняйте access token в хранилище сессии; в onReauthRequired показывайте экран реавторизации.

Порядок провайдеров: built-in ReauthBottomSheet не требует BottomSheetModalProvider. Если host всё же использует BottomSheetModalProvider из @gorhom/bottom-sheet для LogoutConfirmSheet или собственных Gorhom-модалок, которые должны читать Keycloak context, размещайте его внутри KeycloakProvider. Порядок для такого optional случая: 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>

Для Keycloak используйте KeycloakTokenProvider: он участвует в общем single-flight refresh и в безопасном порядке persistence. Не вызывайте отдельный keycloak.updateToken(-1) из собственного provider — это обходит координацию пакета.

setupAxiosInterceptors(api, {
  tokenProvider: new KeycloakTokenProvider(keycloak),
  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 />;

  if (screen === 'resolving') 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 только после исчерпания retry budget; структурированные refresh-ошибки передаются в onRefreshError и shared recovery. Можно использовать isReauthRequired из useKeycloakAuth() как единственный источник правды для отображения реавторизации. Если используете готовый ReauthBottomSheet из пакета, он сам открывается и закрывается по isReauthRequired — достаточно смонтировать компонент и при необходимости передать коллбэки (onSuccess, onDismiss). ReauthBottomSheet is a blocking full-screen overlay, not a Gorhom BottomSheet or BottomSheetModal; @gorhom/bottom-sheet остаётся peer для других UI компонентов/host integration, но этот виджет не управляется Gorhom modal lifecycle. Для кастомной обёртки (свой 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-именем, сеть считается внешней. Это эвристика background unlock, а не требование corporate VPN.

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 retry budget / background > 60с по эвристике host → 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.

Гарантия single-flight распространяется на один React Native JS runtime. API Keychain не предоставляет compare-and-swap, поэтому библиотека не может обеспечить точную взаимную блокировку между несколькими одновременно работающими JS runtime/process без нативного координатора. Поддерживаемая конфигурация основного мобильного приложения использует один runtime; серверная политика ротации Keycloak остаётся последней границей защиты.

Интеграционному приложению не нужно запускать дополнительные параллельные 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. Токены, request body, cookies, authorization headers и client secret в объект ошибки не добавляются.

Решение по refresh-ошибке

| Сигнал | Что делает библиотека | Что должен решить host | |------|------------------------|------------------------| | Подтверждённая terminal OAuth-ошибка: например invalid_grant, invalid_client, unauthorized_client, expired/revoked offline token | Provider auto-refresh does not clear local tokens on terminal refresh errors. Provider-owned invalid_grant enters locked session-invalid / fresh-login recovery and may notify onSessionTerminated; fresh-login recovery keeps the stored tokens locked until explicit logout, successful fresh authorization, or host-owned cleanup. Axios вызывает onRefreshError и передаёт ошибку в shared interactive recovery; текущий Axios interceptor вызывает onSessionTerminated только для invalid_grant и не делает fallback tokenProvider.clearSession()/tokenStorage.clearTokens()/Axios onReauthRequired для остальных structured terminal ошибок. | Очистить свою прикладную сессию и направить пользователя к новому входу. ConfirmAuthPage/ReauthBottomSheet могут передать такой случай в onSessionExpired; для Axios учитывайте documented known limitation. | | KeycloakRefreshOutcomeUnknownError | Результат запроса нельзя считать ни успешным, ни terminal. Попытка durable quarantine в Keychain, чтобы прежний refresh token не был автоматически отправлен повторно. Provider вызывает только onInteractiveRecoveryRequired(error); Axios вызывает onRefreshError(error), но не очищает токены и не вызывает Axios onReauthRequired. | Выбрать интерактивное восстановление (как правило, новый login) и не запускать автоматический повтор refresh с тем же токеном. | | Другая transient/локальная ошибка без подтверждённого terminal OAuth-ответа | Пробрасывается вызывающему коду; библиотека не обещает очистку токенов, показ reauth или deletion session. | Показать recoverable error, проверить сеть/конфигурацию и выбрать дальнейшее действие без предположений о состоянии сессии. |

KeycloakRefreshOutcomeUnknownError и isKeycloakRefreshOutcomeUnknownError экспортируются из корня пакета. Ошибка имеет operation: 'refresh', outcomeUnknown: true и requiresInteractiveRecovery: true; используйте type guard вместо текста message:

import {
  isKeycloakRefreshOutcomeUnknownError,
  type KeycloakRefreshError,
} from '@bmc-soft/keycloak-auth';

function handleRefreshError(error: KeycloakRefreshError | Error) {
  if (isKeycloakRefreshOutcomeUnknownError(error)) {
    navigation.replace('Login');
  }
}

useToken().updateToken() и useKeycloakAuth().updateToken() всегда отклоняются с исходной refresh-ошибкой; KeycloakTokenProvider.refreshToken() не преобразует её в null. Axios onRefreshError получает каждую refresh-ошибку своего instance. ConfirmAuthPage.onError и ReauthBottomSheet.onError вызываются только в описанных выше outer-flow ветках: неверный PIN остаётся внутри PINConfirm, login может открыть fallback, а terminal unlock/reauth при наличии onSessionExpired идёт туда. Provider callbacks (onReauthRequired, onInteractiveRecoveryRequired) относятся только к refresh, которым владеет Provider; callbacks в setupAxiosInterceptors относятся только к этому Axios instance.

При 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 ← инстанс (один раз)
      └─ KeycloakWebViewInjectionProvider ← JS/theme login WebView
          └─ KeycloakThemeProvider ← тема (опционально; мемоизирована)
              └─ TokenProvider     ← токены (частые обновления)
                  └─ ReauthProvider ← состояние реавторизации

Обновление токенов не вызывает ре-рендер компонентов, зависящих только от конфига, инстанса или темы.

Безопасность

  • Токены в OS Keychain (не AsyncStorage)
  • PIN хранится как verifier в Keychain; legacy-хранилище encrypted credentials сохранено только для обратной совместимости
  • Offline token не отправляется в backend и используется только для refresh в Keycloak
  • Запросы по HTTPS
  • Refresh использует offline token. Хранение пароля для интерактивной реавторизации включается отдельно через assistedReauthEnabled (по умолчанию выключено).

Помощь при повторной авторизации

<KeycloakProvider
  config={config}
  redirectUri={redirectUri}
  assistedReauthEnabled
  onAssistedReauthEvent={({phase, reason}) => recordSafeAuthEvent({phase, reason})}
>
  {children}
</KeycloakProvider>

После успешного входа через стандартную форму Keycloak и установки локального PIN библиотека сохраняет проверенные username/password в отдельном OS Keychain-хранилище. Запись привязана к установке приложения, провайдеру, явно настроенным клиентам, аккаунту и текущему grant. PIN не используется как ключ шифрования пароля. Старые legacy-записи не мигрируются.

При необходимости нового входа ConfirmAuthPage сначала требует актуальное локальное подтверждение PIN/биометрией. Под непрозрачным фоном с индикатором выполняется одна подстановка и отправка стандартной формы по доверенному HTTPS-адресу. Страница OTP показывается пользователю; OTP не сохраняется и не заполняется. Если сервер завершает вход без OTP, библиотека следует этому результату. Скрытый этап ограничен 15 секундами; неизвестная форма, ошибка, CAPTCHA или недоступное хранилище оставляют ручной вход.

onAssistedReauthEvent получает только фиксированные phase/reason; пароли, имена пользователей, URL и содержимое страниц в callback не передаются. Явный logout, удаление PIN, смена окружения или отключение уже включённой функции очищают сохранённые данные. Обычная техническая очистка токенов их не удаляет. Пользователям существующих установок потребуется один полный ручной вход через AuthPage с установкой PIN (например, после явного выхода). Обычный ручной ConfirmAuthPage без ранее сохранённой записи не создаёт её: у него нет доказанной привязки к прежнему аккаунту. Внешние IdP и нестандартные разделённые формы username/password остаются ручными.

Экспорты

  • Основной: @bmc-soft/keycloak-auth — провайдер, хуки, экраны, виджеты, UI, хранилище, помощники axios, типы
  • Подпути: @bmc-soft/keycloak-auth/screens, /widgets, /hooks, /axios, /context, /storage (см. exports в package.json)

Лицензия

MIT © BMC-Soft Team