PhoenixPass API

Авторизация

Как B2B-клиенты и сотрудники получают и используют access token

Все ручки eSIM API защищены единой схемой Bearer: access token — это наш JWT (ES256), подписанный identity-контуром сервиса. Проверка токена происходит офлайн по публичному ключу из GET /.well-known/jwks.json — одна и та же middleware стоит на всех ручках, а B2B-клиенты и сотрудники отличаются только набором scopes в токене.

B2B-клиенты (внешний контур)

Сервис сам выступает OAuth2-провайдером. Клиент получает client_id/client_secret (выдаются при создании Application через админку) и обменивает их на access token через POST /oauth/v2/token с грантом client_credentials.

POST /oauth/v2/token
grant_type=client_credentials
client_id=yandex_travel_app
client_secret=***
scope=esim:write esim:read catalog.esim:read

Параметр scope — необязательный пробел-разделённый список запрашиваемых scope. Если не указан, выдаются все allowed_scopes приложения; если указан — выдаётся пересечение запрошенных scope с allowed_scopes. Refresh token не выдаётся: клиент просто запрашивает новый токен, когда старый истёк.

В выпущенном JWT записываются:

ClaimЗначение
issидентификатор identity-контура сервиса
subclient_id приложения
account_idидентификатор аккаунта (тенант: чей пул eSIM, каталог, биллинг)
scopesвыданные scope массивом
exp, iat, jtiстандартные JWT-клеймы

В заголовке JWT передаётся kid для выбора ключа из JWKS.

Бизнес-ручки (/api/v1/*) проверяют подпись JWT без внешних вызовов, сверяют scopes токена с требуемыми scope конкретного метода и достают account_id для tenant-изоляции.

Сотрудники / админка (внутренний контур)

Сотрудники в сервисе не хранятся — точка правды Yandex IdP. Вход происходит через OAuth2 Authorization Code + PKCE:

  1. SPA генерирует code_verifier, считает code_challenge и идёт на GET /api/v1/auth/yandex/login. Backend проверяет redirect_uri по whitelist, подписывает state (CSRF) и редиректит на страницу логина Yandex.
  2. Сотрудник логинится в Яндексе; Yandex редиректит на GET /api/v1/auth/yandex/callback?code=...&state=.... Backend сверяет подпись state и редиректит на SPA с одноразовым authorization code.
  3. SPA обменивает code через POST /oauth/v2/token (grant_type=authorization_code, code + code_verifier).
  4. Backend (не фронт!) обменивает code у Яндекса на opaque-токен и этим токеном сразу запрашивает userinfo. Opaque-токен Яндекса живёт только внутри этого вызова и наружу не выдаётся никогда — фронт его не видит.
  5. Группы из userinfo превращаются в логические роли, роли — в фиксированный набор admin-scopes (identity.account:*, identity.application:*). Политика маппинга выбирается по окружению: на non-production любой сотрудник считается admin, на production действует строгий маппинг групп (у разработчиков доступа нет). Если у сотрудника нет ни одной роли — 403, токен не выдаётся.
  6. Выпускается наш JWT (та же схема Bearer, TTL 1 час) с claims: iss, aud, sub (employee_id из Yandex), scopes, roles, client_id, iat, exp. SPA дальше ходит в админ-методы (/api/v1/admin/*) с Authorization: Bearer <JWT>.

Обращений к IdP на каждый запрос нет — дорогой поход в Yandex происходит один раз при логине (раз в час), дешёвая офлайн-проверка подписи — на каждый запрос, той же middleware, что и для B2B-клиентов. В аудит-лог пишется employee_id (sub из JWT), например «аккаунт создан сотрудником X».

Диагностика

  • getOpenidConfiguration возвращает discovery-документ (token_endpoint, jwks_uri, поддерживаемые grant types и алгоритмы подписи) — используйте его вместо хардкода URL, если ваш OAuth2/OIDC-клиент умеет discovery.
  • 401 Unauthorized — отсутствует/недействителен/истёк Bearer-токен.
  • 403 Forbidden — токен валиден, но не хватает нужного scope, либо неверный aud.

On this page