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.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-auth

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

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 | null
  • refreshToken(): 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. Порядок: KeycloakProviderBottomSheetModalProvider → остальное дерево приложения.

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