Токены, audiences, scopes¶
Статья описывает короткоживущие access token IAM: как их получить обменом, какие в них claims, как устроены audiences и scopes, как сервисы проверяют подпись по JWKS и как безопасно сменить ключ подписи. Для инженеров, подключающих сервис к платформе, и для администраторов.
Принцип: один токен — один сервис¶
Долгоживущие credentials (PAT, секрет service account, upstream-токен IdP)
никогда не уходят в resource service. Их предъявляют только IAM, а взамен
получают access token ровно одного audience со сроком жизни
IAM_TOKEN_TTL_SECONDS (по умолчанию 300 с).
sequenceDiagram
participant C as Клиент (harness / runner)
participant IAM as iam-service
participant CP as control-plane
participant MEM as memory-service
C->>IAM: POST /api/v1/platform-access-tokens:exchange<br/>{token: PAT, audience: "control-plane"}
IAM-->>C: {accessToken (aud=control-plane), expiresIn: 300}
C->>CP: Authorization: Bearer <accessToken>
CP->>CP: проверка подписи по JWKS,<br/>iss, aud == "control-plane", exp
CP-->>C: 200
C-xMEM: тот же токен
MEM-->>C: 401 (audience не совпадает)
Audiences¶
Audience — зарегистрированный в tenant сервис-получатель токенов с реестром
допустимых scopes (allowedScopes). Токен можно выпустить только для
активного audience, и только со scopes из его реестра.
Реестр audiences типовой инсталляции¶
deploy/bootstrap.py заводит и приводит к коду следующие audiences:
| Audience | allowedScopes |
|---|---|
control-plane |
control-plane:read, control-plane:write, control-plane:admin |
memory-service |
memory:read, memory:write, memory:pii, memory:tenants, memory:on-behalf, memory:service |
entitlement-service |
entitlement:check-on-behalf |
policy-service |
policy:check, policy:check-on-behalf, policy:admin |
Смысл каждого scope определяет сервис-владелец — см. Права и scopes.
Scope пишется с префиксом audience
Scope — это строка из allowedScopes целиком: control-plane:write, а не
write. Запрос короткого write к audience control-plane даёт
403 scope_not_allowed. IAM не интерпретирует scopes — он только сверяет
строки с реестром.
Управление audiences¶
# создать
curl -s -X POST "$IAM_URL/api/v1/tenants/$TENANT/audiences" -H "$BT" \
-H 'Content-Type: application/json' \
-d '{"key":"reports","allowedScopes":["reports:read","reports:write"]}'
# список
curl -s "$IAM_URL/api/v1/tenants/$TENANT/audiences" -H "$BT"
# заменить allowedScopes целиком (идемпотентно)
curl -s -X PATCH "$IAM_URL/api/v1/tenants/$TENANT/audiences/reports" -H "$BT" \
-H 'Content-Type: application/json' \
-d '{"allowedScopes":["reports:read","reports:write","reports:admin"]}'
{
"id": "<audience-id>",
"tenant_id": "<tenant-id>",
"key": "reports",
"allowed_scopes": ["reports:admin", "reports:read", "reports:write"],
"status": "active"
}
| Правило | Значение |
|---|---|
key |
^[a-z0-9][a-z0-9._-]{1,118}[a-z0-9]$, уникален в tenant |
| scope | непустая строка до 120 символов, иначе 422 invalid_scope |
| порядок | allowedScopes сортируются и дедуплицируются |
PATCH |
заменяет список целиком; при изменении пишет событие audience.updated; без изменений — no-op |
Ошибки: 404 tenant_not_found, 409 audience_exists, 404 audience_not_found.
Сужение реестра не отзывает выданное
PATCH с более узким списком действует на следующие обмены: scope,
исчезнувший из реестра, перестаёт выдаваться, даже если он есть в потолке
PAT или service account. Уже выданные access token доживают до exp.
Отключить audience (status = disabled) API не позволяет — только в базе.
Обмен PAT на access token¶
curl -s -X POST "$IAM_URL/api/v1/platform-access-tokens:exchange" \
-H 'Content-Type: application/json' \
-d '{"token":"iam_pat_…","audience":"control-plane","scopes":["control-plane:read"]}'
{
"accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
"tokenType": "Bearer",
"expiresIn": 300,
"audience": "control-plane",
"scope": ["control-plane:read"],
"sessionId": "<session-id>"
}
Tenant и principal в запросе не передаются — они берутся из записи PAT.
Access token IAM сам по себе на этом эндпоинте не принимается (не
распознаётся как PAT и даёт 401 invalid_token).
Проверки по порядку:
- PAT действителен (см. Credentials) — иначе
401 invalid_token. audienceвходит вaudiencesPAT и активен в tenant — иначе403 audience_not_allowed.- Запрошенные scopes входят в пересечение
scopeCeiling ∩ allowedScopes— иначе403 scope_not_allowed. - Обновляется
lastUsedAtPAT, в audit пишетсяplatform_access_tokens.exchangeсsession_id.
Эффективные scopes¶
Правило зависит от пути получения токена:
| Путь | Пустой scopes в запросе |
Непустой scopes |
Claim scope_ceiling |
|---|---|---|---|
PAT :exchange |
scopeCeiling ∩ allowedScopes |
должны входить в scopeCeiling ∩ allowedScopes |
scopeCeiling PAT |
federation:exchange |
весь allowedScopes audience |
должны входить в allowedScopes |
allowedScopes audience |
client credentials tokens/exchange |
пустой список | должны входить в scopeCeiling ∩ allowedScopes |
нет |
Service account должен просить scopes явно
У client credentials пустой запрос даёт токен без scopes, а не «весь
потолок». Сервис с таким токеном, скорее всего, получит 403 у
получателя. Всегда передавайте нужные scopes.
Потолок только сужает authority: scope, отсутствующий в allowedScopes
audience, не попадёт в токен, даже если записан в потолке.
Формат access token¶
JWT, подписанный RS256. Заголовок:
Payload токена, полученного обменом PAT человека:
{
"iss": "https://platform.example.com/iam",
"sub": "<principal-id>",
"tenant_id": "<tenant-id>",
"aud": "control-plane",
"scope": ["control-plane:read", "control-plane:write"],
"principal_type": "human",
"credential_id": "<credential-id>",
"scope_ceiling": ["control-plane:read", "control-plane:write"],
"session_id": "<session-id>",
"auth_time": "2026-01-15T10:05:00+00:00",
"acr": "bootstrap",
"iat": 1768471510,
"nbf": 1768471510,
"exp": 1768471810,
"jti": "<uuid>"
}
Claims¶
| Claim | Всегда | Описание |
|---|---|---|
iss |
да | IAM_ISSUER — публичный адрес IAM |
sub |
да | principal_id |
tenant_id |
да | tenant principal |
aud |
да | одна строка, не список |
scope |
да | массив строк (не строка через пробел); может быть пустым |
principal_type |
да | human, agent или service_account |
credential_id |
да | id credential: PAT, service account или external identity (для федерации) — ключ для revocation-кэша сервиса |
iat, nbf, exp |
да | время выпуска и истечения (exp = iat + IAM_TOKEN_TTL_SECONDS) |
jti |
да | уникальный id токена |
scope_ceiling |
PAT, федерация | потолок, от которого считались scopes |
session_id |
PAT, федерация | новый UUID на каждый обмен; попадает в audit IAM |
auth_time |
человек | момент входа в формате ISO 8601 (не число секунд) |
acr |
человек, если известен | уровень аутентификации из authentication context |
Чем отличаются токены разных principals:
principal_type |
auth_time, acr |
scope_ceiling, session_id |
|---|---|---|
human (PAT) |
из authentication context, снятого при выпуске PAT | есть |
human (федерация) |
из upstream-токена текущего входа | есть |
agent (PAT) |
отсутствуют | есть |
service_account |
отсутствуют | отсутствуют |
Чего в токене нет
В токене нет Product, Plan, License, Workspace, Project, Task или
namespace памяти, и нет доменных прав. scope — потолок, а не разрешение:
конкретное право сервис выводит сам (для Control Plane — из binding
principal, см. Авторизация и права).
Проверка токена сервисом¶
Сервисы платформы проверяют токены через
platform-auth-sdk (TokenVerifier + JwksCache).
Требования, которым обязан следовать любой сервис:
| Проверка | Как в SDK |
|---|---|
| Подпись асимметричным алгоритмом | разрешены RS256, RS384, RS512; none и HS* отклоняются до обращения к ключу |
| Точный issuer | iss == настроенный issuer (строковое равенство) |
| Точный audience | aud — строка, равная своему audience; список отклоняется |
| Время | exp, nbf, iat с допуском часов 5 с |
| Обязательные claims | iss, sub, aud, tenant_id, iat, nbf, exp, jti |
| Revocation | локальная политика сервиса (см. ниже) |
Сервис настраивается тремя значениями. Пример для Control Plane из
compose.yml:
CP_IAM_ISSUER: ${TAIMEN_PUBLIC_URL}/iam
CP_IAM_JWKS_URL: http://iam-service:8010/.well-known/jwks.json
CP_IAM_AUDIENCE: control-plane
Issuer публичный, JWKS — внутренний
Issuer должен совпадать с тем, что IAM пишет в iss, — это публичный
адрес ${TAIMEN_PUBLIC_URL}/iam. JWKS при этом берётся по внутреннему
адресу сети compose: проверка подписи не должна зависеть ни от внешнего
прокси, ни от собственного TLS.
Смена issuer — миграция
iss попадает в каждый токен и является частью ключа binding в Control
Plane (issuer + iam_principal_id). Смена TAIMEN_PUBLIC_URL (а с ним
IAM_ISSUER) требует одновременного переноса bindings, иначе вход
закроется для всех. См. Обновление и миграции.
Окно отзыва¶
IAM прекращает обмен отозванных credentials немедленно, но уже выданный
access token криптографически валиден до exp. Закрывать это окно — задача
resource service: SDK предлагает порт RevocationDirectory (например,
проекцию событий credential.revoked / principal.disabled из журнала IAM)
или осознанный режим «окна жизни токена» с ограничением TTL. Поэтому
IAM_TOKEN_TTL_SECONDS стоит держать коротким.
JWKS и ключ подписи¶
{
"keys": [
{"kty": "RSA", "use": "sig", "alg": "RS256", "kid": "local-dev", "n": "…", "e": "AQAB"}
]
}
- Ключ — RSA в PEM без пароля:
IAM_SIGNING_PRIVATE_KEY(строкой) илиIAM_SIGNING_PRIVATE_KEY_FILE(файлом; в compose — docker-секрет).make secretsгенерирует RSA 3072. kid— значениеIAM_SIGNING_KEY_ID.- JWKS публикует только текущий ключ. Предыдущий ключ после смены в JWKS не остаётся.
- Без ключа обмен токенов невозможен: IAM отвечает ошибкой 500
(
IAM_SIGNING_PRIVATE_KEY is required for token exchange), а JWKS не отдаётся.
Кэш ключей на стороне сервиса (JwksPolicy SDK по умолчанию):
| Параметр | По умолчанию | Смысл |
|---|---|---|
refresh_after_seconds |
300 | после этого возраста промах по kid вызывает обновление |
min_refresh_interval_seconds |
10 | не чаще одного обновления за интервал |
stale_after_seconds |
3600 | дольше этого непроверенный кэш не используется → verification_unavailable (503) |
Процедура ротации ключа подписи¶
Поскольку JWKS содержит один ключ, после смены ключа все ранее выданные
access token (не старше IAM_TOKEN_TTL_SECONDS) перестают проходить проверку.
Клиенты получат 401 и обменяют credential заново. Процедура:
-
Сгенерируйте новый ключ:
-
В
.envукажите новый файл и новыйkid: -
Пересоздайте контейнер:
docker compose up -d iam-service. - Проверьте JWKS (новый
kid) и пробный обмен PAT. - В течение
IAM_TOKEN_TTL_SECONDSклиенты получат401на старые токены и перевыпустят их; клиентыcontrol-plane-clientиServiceTokenProviderобменивают credential заново сами. - Удалите старый файл ключа.
Всегда меняйте kid
Если оставить прежний kid с новым ключом, сервисы не поймут, что ключ
сменился: кэш с тем же kid будет считаться годным до
refresh_after_seconds, и все новые токены будут отклоняться с ошибкой
подписи до пяти минут.
Ошибки обмена¶
| HTTP | detail |
Путь | Причина |
|---|---|---|---|
| 401 | invalid_token |
PAT | PAT недействителен по любой причине |
| 401 | invalid_client |
client credentials | неверный clientId/clientSecret, отозванный service account, неактивный principal |
| 403 | audience_not_allowed |
все | audience не входит в credential или не активен |
| 403 | scope_not_allowed |
все | запрошен scope вне потолка или реестра (частая причина — scope без префикса) |
| 422 | — | все | лишние поля в теле, пустой token/audience |
| 500 | — | все | не настроен ключ подписи |
Ошибки проверки токена на стороне сервиса (invalid_token,
verification_unavailable, insufficient_scope) описаны в
platform-auth-sdk и
Кодах ошибок.