Перейти к основному содержимому

@mineflow/auth-native

@mineflow/auth-native — это L3-адаптер аутентификации client-SDK для React Native. Он содержит один класс — ReactNativeTokenProvider, который реализует порт TokenProvider из @mineflow/client-core поверх результата нативного OIDC-флоу (типично — react-native-app-auth, но не привязан к нему).

По слоистой архитектуре ADR-0042 всё платформо-специфичное инъектируется адаптерами, чтобы ядро (fetch, идемпотентность, ошибки, саги, пагинация, SSE) писалось один раз и работало в web и RN. На web эту роль играет @mineflow/auth-web поверх keycloak-js; в RN использовать keycloak-js нельзя (browser-only — window, redirect-флоу), поэтому логин идёт через нативный OIDC, а auth-native превращает его токены в порт, который понимает ядро: «дай актуальный access-token и канонические роли».

к сведению

Пакет не зависит от react-native-app-auth напрямую. Приложение само владеет auth-состоянием и прокидывает в адаптер две функции — getState (вернуть текущее состояние) и refresh (дёрнуть нативный refresh). За счёт этого адаптер тестируем, бандл-безопасен и не привязан к конкретной OIDC-библиотеке RN — подойдёт и expo-auth-session.

Установка

pnpm add @mineflow/auth-native
# нативный OIDC-флоу в самом RN-приложении (peer, не зависимость SDK):
pnpm add react-native-app-auth

Единственная runtime-зависимость пакета — @mineflow/client-core (workspace:*), откуда берутся порт TokenProvider, тип SystemRole и хелперы декодирования JWT. В монорепо MineFlow пакеты резолвятся как workspace:*.

примечание

react-native-app-auth указан как пример: SDK его не импортирует. Подойдёт любая библиотека, дающая accessToken / refreshToken / дату истечения. Полный RN-стек (помимо auth) требует ещё пары адаптеров — см. React Native ниже.

Использование

Логином и хранением auth-состояния владеет приложение. Адаптер только читает это состояние и при необходимости упреждающе рефрешит токен.

import { authorize, refresh } from 'react-native-app-auth';
import { ReactNativeTokenProvider } from '@mineflow/auth-native';

const config = {
issuer: 'https://auth.mineflow.local/realms/mineflow',
clientId: 'mineflow-mobile',
redirectUrl: 'com.mineflow://oauthredirect',
scopes: ['openid', 'profile', 'roles'],
};

// 1. Логин в нативном браузере; состояние хранит приложение
let authState = await authorize(config);
// authState: { accessToken, refreshToken, accessTokenExpirationDate, ... }

// 2. Адаптер отдаёт ядру актуальный токен (упреждающий refresh) + роли
const tokenProvider = new ReactNativeTokenProvider({
getState: () => authState,
refresh: async () => {
authState = await refresh(config, { refreshToken: authState.refreshToken! });
return authState;
},
});

Дальше провайдер передаётся в MineflowProvider из @mineflow/client-react. Роли резолвятся из access-token синхронно через getRoles():

import 'react-native-get-random-values'; // polyfill для crypto.randomUUID
import { MineflowProvider } from '@mineflow/client-react';

<MineflowProvider
baseUrl={API_BASE} // origin API; пути из api-client уже содержат /api/v1
tokenProvider={tokenProvider}
generateId={() => crypto.randomUUID()}
roles={tokenProvider.getRoles()}
>
{children}
</MineflowProvider>;

После этого все хуки client-react (useAssets, useApproveShiftReport, …) и RBAC-гейты работают как на web — UI пишется на RN-примитивах, бизнес-логика переиспользуется как есть.

Ключевые экспорты

Пакет реэкспортирует ровно три символа из react-native-token-provider.

ЭкспортТипНазначение
ReactNativeTokenProviderclass implements TokenProviderадаптер: упреждающий/реактивный refresh + маппинг ролей
ReactNativeTokenProviderOptionsinterfaceопции конструктора (getState, refresh, …)
NativeAuthStateinterfaceподмножество результата нативного OIDC, которое читает адаптер

ReactNativeTokenProviderOptions

ОпцияТипПо умолчаниюНазначение
getState() => NativeAuthState | nullвернуть текущее auth-состояние (хранит приложение)
refresh() => Promise<NativeAuthState>дёрнуть нативный refresh, сохранить и вернуть новое состояние
minValiditySeconds?number30запас до истечения для упреждающего refresh
now?() => numberDate.nowисточник «сейчас» в мс (для тестов)

NativeAuthState

Подмножество AuthorizeResult / RefreshResult из react-native-app-auth — адаптеру нужны только эти поля:

interface NativeAuthState {
accessToken: string;
refreshToken?: string;
/** ISO datetime истечения access-token (поле accessTokenExpirationDate). */
accessTokenExpirationDate?: string;
}

Методы порта TokenProvider

  • getToken(opts?) — если opts.forceRefresh (выставляется ядром реактивно при ответе 401) или access-token истекает в ближайшие minValiditySeconds, вызывает refresh() и отдаёт свежий accessToken; иначе возвращает текущий. При ошибке refresh() возвращается текущий токен (API ответит 401 → UI инициирует повторный login). Если getState() вернул nullgetToken() отдаёт null.
  • getRoles() — декодирует realm-роли из access-token (через realmRolesFromToken из client-core) и маппит их в канонические SystemRole (mapKeycloakRoles). При отсутствии состояния возвращает [].
примечание

forceRefresh адаптером поддержан полностью: ядро (makeAuthenticatedFetch из client-core) при 401 повторяет запрос с { forceRefresh: true }. Это покрывает случаи, когда упреждающий refresh не сработал или токен протух «в полёте» — например отложенная offline-мутация.

Подводные камни и важные правила

Источник истины по состоянию — приложение, не адаптер

Адаптер не хранит токены и не вызывает authorize. Внутри refresh обязательно сохрани новое состояние туда же, откуда читает getState — иначе следующий getToken() снова получит протухший токен. В примере выше это общая переменная authState; в реальном приложении — стейт-стор / secure storage.

Упреждающий refresh работает только с датой истечения

Окно «истекает скоро» считается из accessTokenExpirationDate. Если поле не передано (или не парсится как дата), isExpiringSoon всегда false — упреждающего refresh не будет, останется только реактивный по 401. react-native-app-auth это поле отдаёт; при другой OIDC-библиотеке убедись, что мапишь его в NativeAuthState.

Деградация refresh безопасна

Если refresh() бросил, getToken() молча отдаёт текущий (возможно протухший) токен — запрос уйдёт, бэк ответит 401, и UI начнёт login заново. Не нужно ловить ошибки refresh внутри адаптера — это сделано by design.

keycloak-js в RN не используется

keycloak-js — browser-only. Для RN канон — нативный OIDC + ReactNativeTokenProvider. Не пытайся тянуть @mineflow/auth-web в React Native.

React Native

auth-native закрывает только аутентификацию. Полный RN-стек требует ещё двух платформенных адаптеров, инъектируемых через тот же MineflowProvider:

  • generateId — нативного crypto.randomUUID нет: подключи react-native-get-random-values (импорт до использования) и передай generateId={() => crypto.randomUUID()}. Нужен для авто-Idempotency-Key на всех write.
  • SSE-fetch — дефолтный RN-fetch не отдаёт потоковый ReadableStream, поэтому useNotificationFeed без потокового fetch не заработает. Передай в client-core свой fetchImpl с потоковой поддержкой (react-native-fetch-api).

Подробности по сквозному RN-стеку — в рецепте React Native. По RBAC-гейтам поверх getRoles() — рецепт RBAC.

Статус

Адаптер написан и покрыт юнит-тестами (логика refresh, маппинг ролей), но в живом RN-рантайме ещё не прогонялся (нет RN-приложения на этом адаптере). По бандлу browser/RN-safe.

Ссылки