Авторизация
Как 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-контура сервиса |
sub | client_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:
- SPA генерирует
code_verifier, считаетcode_challengeи идёт наGET /api/v1/auth/yandex/login. Backend проверяетredirect_uriпо whitelist, подписываетstate(CSRF) и редиректит на страницу логина Yandex. - Сотрудник логинится в Яндексе; Yandex редиректит на
GET /api/v1/auth/yandex/callback?code=...&state=.... Backend сверяет подписьstateи редиректит на SPA с одноразовым authorization code. - SPA обменивает code через
POST /oauth/v2/token(grant_type=authorization_code,code+code_verifier). - Backend (не фронт!) обменивает code у Яндекса на opaque-токен и этим токеном сразу запрашивает userinfo. Opaque-токен Яндекса живёт только внутри этого вызова и наружу не выдаётся никогда — фронт его не видит.
- Группы из userinfo превращаются в логические роли, роли — в фиксированный
набор admin-scopes (
identity.account:*,identity.application:*). Политика маппинга выбирается по окружению: на non-production любой сотрудник считается admin, на production действует строгий маппинг групп (у разработчиков доступа нет). Если у сотрудника нет ни одной роли —403, токен не выдаётся. - Выпускается наш 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.