JWT и защита API
Как устроен JWT, что на самом деле гарантирует подпись, как ASP.NET Core проверяет токен и где в этой проверке ошибаются. В конце — как передать идентичность пользователя через цепочку сервисов и API Gateway, не раздавая токены с доступом ко всей системе.
Перед этой темой: OAuth 2.0 и OpenID Connect.
Из чего состоит JWT и что гарантирует подпись?
Заголовок раздела «Из чего состоит JWT и что гарантирует подпись?»Что это
JWT — компактный токен вида xxxxx.yyyyy.zzzzz. Его выдаёт сервер авторизации, а клиент отправляет как bearer-токен в заголовке Authorization: Bearer ....
Из чего состоит
- Header — алгоритм подписи и тип:
{"alg":"RS256","typ":"JWT"}. - Payload — claims, то есть утверждения о пользователе и о самом токене:
sub— кто это,exp— до какого момента токен действует. - Signature — подпись над строкой
header.payload.
Header и payload — обычный JSON в base64url. Это кодирование, а не шифрование.
Частая ошибка
Считать содержимое токена скрытым. Любой, кто видит токен, декодирует base64url и прочитает все claims. Поэтому пароли и персональные данные в payload не кладут.
Как устроено
- Подпись считается над строкой
base64url(header) + "." + base64url(payload)алгоритмом изalg. Изменение одного символа в payload ломает подпись. - Алгоритмы: HS256 — HMAC-SHA256 с общим секретом, RS256 и PS256 — RSA, ES256 — ECDSA на кривой P-256. Поле
kidв заголовке говорит, каким из ключей издателя подписан токен. - Стандартные claims (RFC 7519) необязательны. Обязательными их делает профиль, например RFC 9068 для access-токенов.
| Claim | Что означает |
|---|---|
iss |
кто выпустил токен |
sub |
о ком токен: id пользователя или клиента |
aud |
для кого: какой API должен его принять |
exp, nbf |
действует до и не раньше; секунды Unix-времени |
iat, jti |
когда выпущен; уникальный id токена |
Когда что выбирать
| JWS | JWE | |
|---|---|---|
| Частей в компактной форме | 3 | 5 |
| Защищает | целостность и подлинность | ещё и конфиденциальность |
| Кто читает payload | любой | владелец ключа расшифровки |
Ловушки
- Подписан не значит годен. Подпись ничего не говорит о сроке, издателе и получателе — их проверяют отдельно.
- HS256 симметричен. Кто может проверить токен, тот может и выпустить его.
Правило
Access-токены подписывают асимметрично (RS256, ES256), а в payload кладут идентификаторы и права, но не персональные данные.
Под капотом
- Компактная форма JWS (RFC 7515) — три части в base64url без
=в конце, через точку. У JWE (RFC 7516) пять частей: заголовок, зашифрованный ключ, вектор инициализации, шифротекст и тег аутентификации. - Вложенный JWT — сначала подпись, потом шифрование: JWE, внутри которого JWS. Так payload и подписан, и скрыт.
- Ключ для проверки берётся не из токена, а из конфигурации проверяющего.
kidлишь выбирает ключ из заранее известного набора.
Цена
- Размер. base64url добавляет треть к JSON, подпись RS256 с 2048-битным ключом — ещё 256 байт, или 342 символа после кодирования. Токен с сотней ролей легко весит несколько килобайт и едет в каждом запросе.
- CPU. Проверка подписи не ходит в сеть, но выполняется на каждом запросе в каждом сервисе.
Компромиссы
| HS256 | RS256, ES256 | |
|---|---|---|
| Кто проверяет | владельцы общего секрета | любой с открытым ключом |
| Кто может выпустить | любой проверяющий | только владелец закрытого ключа |
| Где уместна | один сервис сам выпускает и проверяет | сервер авторизации и много API |
Самодостаточность — тоже компромисс: проверка без сети, но отозвать токен до exp без дополнительного состояния нельзя.
Как проверить в проде
- Декодировать токен локально (
JsonWebTokenHandler.ReadJsonWebToken) и посмотретьalg,kid,iss,aud,exp. Боевые токены в сторонние онлайн-декодеры не вставляют. - Размер токенов виден по заголовку
Authorizationна прокси: ответы 400 или 431 означают, что превышен лимит заголовков.
Итог: JWT — подписанное, а не секретное утверждение издателя; асимметричная подпись отделяет того, кто выпускает токены, от тех, кто их проверяет.
using System.Security.Cryptography;using Microsoft.IdentityModel.JsonWebTokens;using Microsoft.IdentityModel.Tokens;
var key = new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32)); // 256 случайных битvar handler = new JsonWebTokenHandler();var parameters = new TokenValidationParameters{ ValidIssuer = "https://auth.example.com", ValidAudience = "orders-api", IssuerSigningKey = key,};
string token = handler.CreateToken(new SecurityTokenDescriptor{ Issuer = "https://auth.example.com", Audience = "orders-api", Claims = new Dictionary<string, object> { ["sub"] = "42" }, Expires = DateTime.UtcNow.AddMinutes(5), SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256),});Console.WriteLine(token.Split('.').Length); // 3: header.payload.signature
// Прочитать можно без ключа: payload только закодированvar jwt = handler.ReadJsonWebToken(token);Console.WriteLine($"{jwt.Alg} {jwt.Subject}"); // HS256 42
// Меняем sub в payload — подпись больше не сходитсяvar parts = token.Split('.');var payload = Base64UrlEncoder.Decode(parts[1]).Replace("\"42\"", "\"1\"");var forged = $"{parts[0]}.{Base64UrlEncoder.Encode(payload)}.{parts[2]}";
Console.WriteLine((await handler.ValidateTokenAsync(token, parameters)).IsValid); // TrueConsole.WriteLine((await handler.ValidateTokenAsync(forged, parameters)).IsValid); // FalseHS256 здесь нужен, чтобы пример был самодостаточным. В проде access-токены подписывает сервер авторизации асимметричным ключом.
{ "alg": "RS256", "kid": "2026-10", "typ": "at+jwt" }
{ "iss": "https://auth.example.com", "sub": "42", "aud": "orders-api", "exp": 1791332100, "nbf": 1791331200, "iat": 1791331200, "jti": "5f0c6a9e-3b1d-4c55-9a7e-2d8f1b6e4c10", "scope": "orders.read"}Первый объект — header, второй — payload. Токен выпущен 7 октября 2026 года в 00:00 UTC и действует 15 минут; typ: at+jwt помечает access-токен по RFC 9068.
- Чем base64url отличается от base64? —
-и_вместо+и/и нет=в конце: строку можно вставить в URL и заголовок. - Можно ли продлить токен, поменяв
exp? — Нет: изменение payload ломает подпись, и токен не пройдёт проверку. - Какие claims обязательны по RFC 7519? — Никакие; обязательность задают профили, например RFC 9068 для access-токенов.
- Зачем
kidв заголовке? — Выбрать один из нескольких опубликованных ключей издателя; без этого не обойтись при смене ключей. - Когда нужен JWE? — Когда payload нужно скрыть от клиента и промежуточных узлов; чаще проще не класть туда секретов.
Как ASP.NET Core проверяет JWT?
Заголовок раздела «Как ASP.NET Core проверяет JWT?»Что это
JWT Bearer — схема аутентификации: клиент присылает токен в заголовке Authorization: Bearer ..., а API проверяет его и узнаёт пользователя. Токен выдаёт сервер авторизации, API только проверяет.
Как пользоваться
- Установите пакет
Microsoft.AspNetCore.Authentication.JwtBearer. - Вызовите
AddAuthentication().AddJwtBearer(...)и задайтеAuthorityиAudience. - Пометьте эндпоинты
RequireAuthorization()или[Authorize]. - В обработчике пользователь доступен как
HttpContext.User— его claims пришли из токена.
Что проверяется
Подпись, издатель (iss), получатель (aud) и срок (exp). Если что-то не так — ответ 401.
Частая ошибка
Отключить проверку получателя (ValidateAudience = false), чтобы «заработало». Тогда API примет токен, выпущенный для любого другого API того же сервера авторизации.
Как устроено
- При первом запросе обработчик загружает
{Authority}/.well-known/openid-configuration, а по ссылкеjwks_uriиз него — JWKS. Оттуда берутся ожидаемыйissи ключи подписи. JsonWebTokenHandler(.NET 8+) выбирает ключ поkidи проверяет подпись.- Проверяются
iss,aud(значение изAudience),expиnbfс допускомClockSkew. - Claims становятся
ClaimsPrincipalвHttpContext.User. При ошибке — 401 и заголовокWWW-Authenticate: Bearer error="invalid_token".
Параметры TokenValidationParameters
| Параметр | По умолчанию |
|---|---|
ValidateIssuer, ValidateAudience, ValidateLifetime |
true |
RequireSignedTokens, RequireExpirationTime |
true |
ClockSkew |
5 минут |
ValidAlgorithms, ValidTypes |
не заданы: подходит любой |
Ловушки
- Маппинг claims.
MapInboundClaimsпо умолчаниюtrue:subстановитсяhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier. Выключите его и задайтеNameClaimTypeиRoleClaimType. - Код времён .NET 7.
context.SecurityToken as JwtSecurityTokenв событиях с .NET 8 даётnull: тамJsonWebToken. Старое поведение возвращаетUseSecurityTokenValidators = true. RequireHttpsMetadata. По умолчаниюtrue; метаданные по HTTP допустимы только при разработке.
Правило
Задавайте Authority и Audience, не отключайте проверки, а для коротких токенов уменьшайте ClockSkew до 30–60 секунд.
Под капотом
JwtBearerHandlerдержитConfigurationManager<OpenIdConnectConfiguration>. Метаданные и JWKS он перечитывает раз в 12 часов (AutomaticRefreshInterval).- Если ключа с таким
kidнет,RefreshOnIssuerKeyNotFound(по умолчаниюtrue) запрашивает внеплановое обновление. Оно выполняется не чаще раза в 5 минут (RefreshInterval). - С .NET 8 менеджер конфигурации передаётся в
TokenValidationParameters.ConfigurationManager.JsonWebTokenHandlerзапоминает last known good — последнюю конфигурацию, с которой токены успешно проверялись.
Цена
- В сеть проверка не ходит: только разбор и подпись. Загрузку метаданных оплачивает первый запрос после старта, и недоступный в этот момент сервер авторизации ломает аутентификацию.
ClockSkewприбавляется к каждомуexp: токен на 5 минут фактически живёт 10.
Компромиссы
- Ротация ключей. Из-за кеша новый ключ должен появиться в JWKS заранее, а старый — оставаться до истечения последнего подписанного им токена. Иначе между ротацией и обновлением кеша часть запросов получит 401.
- Обработчики токенов.
JsonWebTokenHandlerбыстрее примерно на 30% и асинхронный.JwtSecurityTokenHandlerизSystem.IdentityModel.Tokens.Jwtостаётся ради совместимости. - Несколько издателей. Одна схема — один
Authority. Для двух серверов авторизации регистрируют две схемы и выбирают нужную черезAddPolicyScheme.
Как проверить в проде
- Отказ
JwtBearerHandlerпишет в лог с уровнемInformation, поэтому для категорииMicrosoft.AspNetCore.Authenticationуровень понижают. Причину видно по коду:IDX10223— токен истёк,IDX10214— не тотaud,IDX10205— не тотiss. - Событие
OnAuthenticationFailed— место для метрики отказов по типу исключения.
Итог: проверка JWT — локальная операция по закешированным ключам; в проде её надёжность определяют ротация ключей, ClockSkew и понятные логи отказов.
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer(options =>{ // Discovery: {Authority}/.well-known/openid-configuration → issuer и jwks_uri options.Authority = "https://auth.example.com"; options.Audience = "orders-api"; // ожидаемый aud options.MapInboundClaims = false; // sub остаётся "sub", а не длинным URI options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; options.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(30); // вместо 5 минут});builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
// Без токена — 401, с валидным токеном — значение subapp.MapGet("/me", (ClaimsPrincipal user) => user.FindFirst("sub")?.Value) .RequireAuthorization();
app.Run();- Ходит ли API к серверу авторизации на каждый запрос? — Нет: метаданные и ключи кешируются и обновляются раз в 12 часов или при неизвестном
kid. - Почему просроченный токен ещё несколько минут работает? —
ClockSkewпо умолчанию 5 минут. - Почему
User.FindFirst("sub")возвращаетnull? —MapInboundClaimsпревратилsubвClaimTypes.NameIdentifier. - Что изменилось в JwtBearer в .NET 8? — По умолчанию
JsonWebTokenHandler, в событияхJsonWebToken; вернуть старое —UseSecurityTokenValidators = true. - Как принимать токены двух серверов авторизации? — Две схемы
AddJwtBearerиAddPolicyScheme, который выбирает схему, например, поiss.
Какие уязвимости и ошибки бывают с JWT?
Заголовок раздела «Какие уязвимости и ошибки бывают с JWT?»Что это
Ошибки с JWT почти всегда сводятся к двум: API принимает токен, который не должен принять, или токен раскрывает лишнее.
Частые ошибки
- Неполная проверка. С отключёнными
ValidateAudienceилиValidateLifetimeпроходит токен для другого сервиса или просроченный токен. - Слабый секрет. HS256 с секретом вроде
"secret123"подбирается перебором по одному перехваченному токену, и атакующий выпускает любые токены. - Секреты в payload. Payload читает любой, кто увидел токен: в логах, в браузере, на прокси.
- Долгая жизнь. Токен на сутки, украденный из лога, работает сутки: штатно его не отозвать.
Как защититься
Включённые проверки, access-токен на 5–15 минут, в payload — только идентификаторы.
Как устроено
Токен приносит клиент, поэтому и заголовок, и payload под контролем атакующего. Атаки используют то, что API доверяет им до проверки.
| Ошибка | Последствие | Защита |
|---|---|---|
Принимать alg: none |
токен без подписи проходит | RequireSignedTokens (по умолчанию true), ValidAlgorithms |
Брать алгоритм из alg для любого ключа |
RS256 → HS256 с открытым ключом как секретом | алгоритм привязан к типу ключа, ValidAlgorithms |
| Короткий или словарный секрет HS256 | перебор офлайн и свои токены | случайный ключ от 256 бит или асимметричная подпись |
Не проверять aud |
принимается токен чужого API | Audience и ValidateAudience = true |
| Токен живёт часы и дни | украденный токен долго работает | 5–15 минут и refresh-токен у клиента |
| Персональные данные в payload | утечка через логи и браузер | только идентификаторы |
| Сотни ролей и групп в токене | заголовок больше лимита прокси, 400 или 431 | права загружать в API, а не возить в токене |
Ловушки
- Отзыв. Блокировка пользователя или выход из системы не отменяют уже выданный access-токен. Нужен механизм сверху: список отозванных
jtiили интроспекция. ValidateIssuerSigningKey. Подпись проверяется и без него. Флаг включает дополнительную проверку самого ключа, например срока сертификата, и по умолчанию выключен.
Правило
Алгоритм, ключ, издатель, получатель и тип берутся из конфигурации API, а не из токена.
Под капотом
- Подмена алгоритма. Уязвимая библиотека вызывает
verify(token, key)с алгоритмом изalg. Если API знает открытый RSA-ключ, а атакующий ставит HS256, библиотека считает HMAC с открытым ключом как секретом — а он опубликован в JWKS.Microsoft.IdentityModelпроверяет, что алгоритм подходит к типу ключа, поэтому RSA-ключ для HS256 не используется. - Ключ из токена. Заголовки
jwk,jkuиx5uпередают ключ или его URL прямо в токене. Проверяющий, который им доверяет, проверит подпись ключом атакующего, а запрос поjkuиx5u— готовый SSRF.kid— тоже ввод атакующего: RFC 8725 предупреждает об SQL- и LDAP-инъекциях через него. - Путаница токенов. id_token или токен для другого API подписан тем же издателем и проходит проверку подписи. Защита —
audи явныйtyp, напримерat+jwtиз RFC 9068 черезValidTypes. - Слабый ключ. Актуальный
Microsoft.IdentityModelотклоняет ключ HS256 короче 256 бит (IDX10720), но длина не гарантирует энтропию: пароль из словаря длиной 40 символов всё ещё подбирается.
Компромиссы
| Подход | Плюс | Минус |
|---|---|---|
| Короткий JWT без отзыва | нет состояния и сети | окно до exp после кражи |
JWT и список отозванных jti |
быстрый отзыв | общее хранилище, проверка на каждом запросе |
| Reference-токен и интроспекция | мгновенный отзыв, payload скрыт | запрос к серверу авторизации; кеш снова даёт окно |
Как проверить в проде
- Негативные тесты в CI: токен с
alg: none, с чужимaud, просроченный и подписанный чужим ключом получают 401. - Заголовок
Authorizationне должен целиком попадать в логи и трассировки.
Итог: JWT безопасен настолько, насколько API не доверяет токену; отзыв — осознанный выбор между состоянием и окном уязвимости.
using System.Collections.Concurrent;using Microsoft.AspNetCore.Authentication.JwtBearer;using Microsoft.IdentityModel.JsonWebTokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<RevokedTokens>();builder.Services.AddAuthentication().AddJwtBearer(options =>{ options.Authority = "https://auth.example.com"; options.Audience = "orders-api"; // проверку aud не отключаем options.TokenValidationParameters.ValidAlgorithms = ["RS256"]; // ни none, ни HS256 options.TokenValidationParameters.ValidTypes = ["at+jwt"]; // только access-токены
// Отзыв до exp: jti из списка отозванных — 401 options.Events = new JwtBearerEvents { OnTokenValidated = context => { var revoked = context.HttpContext.RequestServices.GetRequiredService<RevokedTokens>(); if (context.SecurityToken is not JsonWebToken jwt || revoked.ContainsKey(jwt.Id)) context.Fail("Token revoked"); return Task.CompletedTask; } };});
var app = builder.Build();app.MapGet("/orders", () => "ok").RequireAuthorization();app.Run();
// jti → exp; в проде — общее хранилище, например Redis с TTL до exppublic class RevokedTokens : ConcurrentDictionary<string, DateTimeOffset>;ValidTypes = ["at+jwt"] подходит, только если сервер авторизации ставит такой typ; многие ставят JWT.
- Почему не стоит возить в токене все роли и группы? — Размер растёт в каждом запросе, а изменения прав не видны до нового токена.
- Как отозвать JWT до истечения? — Список отозванных
jtiдо ихexpили reference-токены с интроспекцией. - Почему пароль нельзя использовать как ключ HS256? — Мало энтропии: ключ подбирается офлайн по одному токену; RFC 8725 это прямо запрещает.
- Чем опасен
kid? — Это ввод атакующего; его только ищут в известном наборе ключей и не подставляют в SQL- и LDAP-запросы. - Зачем
typ: at+jwt? — Отличить access-токен от id_token и других JWT того же издателя.
Как передавать идентичность между сервисами и через API Gateway?
Заголовок раздела «Как передавать идентичность между сервисами и через API Gateway?»Что это
В микросервисах запрос пользователя проходит несколько сервисов: шлюз → orders → billing. Каждому нужно знать, кто вызывает и от чьего имени.
Как пользоваться
- Проброс токена. Сервис кладёт полученный токен пользователя в заголовок
Authorizationсвоего запроса к следующему сервису. - Client credentials. Сервис получает собственный токен по
client_idи секрету или сертификату. Так работают фоновые задачи, где пользователя нет. - API Gateway. Единая точка входа: маршрутизирует запросы, проверяет токен, ограничивает частоту. Токен идёт дальше в сервисы.
Частая ошибка
Проверять токен только на шлюзе, а сервисам доверять заголовку X-User-Id. Любой, кто попал во внутреннюю сеть, подставит туда чужой id.
Как устроено
| Способ | Чья идентичность в токене | Риск |
|---|---|---|
| Проброс токена | пользователя | широкий aud: токен годен во всех сервисах |
| Client credentials | сервиса | пользователь теряется, сервис действует своими правами |
| Token exchange | пользователя в sub, сервиса в act |
запрос к серверу авторизации на каждый переход |
Token exchange по шагам
- orders-api получает токен пользователя с
aud=orders-api. - Отправляет его на token endpoint с
grant_type=urn:ietf:params:oauth:grant-type:token-exchange,subject_tokenиaudience=billing-api, аутентифицируясь своимclient_id. - Сервер авторизации проверяет политику и выдаёт токен с
aud=billing-api, тем жеsubи, при делегировании,actс идентичностью orders-api. - billing-api проверяет токен как обычно и видит и пользователя, и вызывающий сервис.
В Microsoft Entra ID тот же сценарий — On-Behalf-Of flow; в .NET его берёт на себя Microsoft.Identity.Web.
Ловушки
- Confused deputy. Сервис с client credentials выполняет запрос пользователя своими широкими правами. Если он сам не проверил права пользователя, тот получает доступ через сервис.
- Широкий
aud. Проброс работает, только если токен выпущен сразу для всех сервисов, — это ключ от всей системы.
Правило
Есть пользователь — token exchange или On-Behalf-Of. Нет пользователя — client credentials. Проброс токена — только внутри одной границы доверия.
Под капотом
- Шлюз проверяет подпись,
iss,audи срок, отсекает мусор и применяет лимиты. Сервис за ним проверяет токен снова, локально по закешированному JWKS, без сетевого вызова. - Phantom token. Снаружи клиент держит непрозрачный reference-токен, шлюз интроспектирует его и передаёт внутрь JWT. Claims не утекают наружу, а внутри проверка остаётся локальной.
- mTLS шифрует трафик и доказывает, какой сервис звонит. Service mesh (Istio, Linkerd) включает его без кода. RFC 8705 привязывает токен к сертификату клиента через claim
cnf. Где клиентский сертификат неудобен, например в SPA, ту же задачу решает DPoP (RFC 9449): клиент подписывает каждый запрос своим ключом.
Цена
- Token exchange — запрос к серверу авторизации на каждый переход. Обменянный токен кешируют по паре «пользователь — audience» до
exp, иначе сервер авторизации становится узким местом. - Короткие токены сужают окно после кражи, но учащают выпуск и обмен.
Компромиссы
- Проверка только на шлюзе дешевле, но делает внутреннюю сеть зоной доверия: обход шлюза или взломанный сервис внутри сети дают полный доступ. Zero trust — проверка в каждом сервисе плюс mTLS.
- YARP как шлюз. Аутентификацию делает обычный
AddJwtBearer, маршрутам назначаютAuthorizationPolicy. ЗаголовокAuthorizationпо умолчанию уходит в сервис как есть; замену или обмен токена делают в request transform.
Как проверить в проде
subиactв логах и трассировках показывают, от чьего имени и кто действовал на каждом шаге.- Тест: запрос к сервису мимо шлюза без токена или с токеном для чужого
audполучает 401.
Итог: пользователь доходит до конца цепочки через обмен токенов с узким aud, а доверие к сети заменяют проверка токена в каждом сервисе и mTLS.
using System.Net.Http.Json;using System.Text.Json;
using var http = new HttpClient();string incomingToken = "eyJ..."; // из Authorization входящего запроса, aud=orders-api
// orders-api обменивает токен пользователя на токен для billing-apivar response = await http.PostAsync("https://auth.example.com/connect/token", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "urn:ietf:params:oauth:grant-type:token-exchange", ["subject_token"] = incomingToken, ["subject_token_type"] = "urn:ietf:params:oauth:token-type:access_token", ["audience"] = "billing-api", // для кого нужен новый токен ["client_id"] = "orders-api", // кто обменивает: сервис аутентифицируется сам ["client_secret"] = "from-secret-store", }));
// Новый токен: aud=billing-api, sub пользователя, act={"sub":"orders-api"}var json = await response.Content.ReadFromJsonAsync<JsonElement>();string billingToken = json.GetProperty("access_token").GetString()!;Сервер авторизации должен поддерживать token exchange: в Keycloak он стандартный с версии 26.2, в Duende IdentityServer реализуется как extension grant.
// NuGet: Yarp.ReverseProxy, Microsoft.AspNetCore.Authentication.JwtBearerusing Yarp.ReverseProxy.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer(options =>{ options.Authority = "https://auth.example.com"; options.Audience = "orders-api";});builder.Services.AddAuthorization();builder.Services.AddReverseProxy().LoadFromMemory( [new RouteConfig { RouteId = "orders", ClusterId = "orders", AuthorizationPolicy = "default", // без валидного токена — 401 уже на шлюзе Match = new RouteMatch { Path = "/orders/{**rest}" }, }], [new ClusterConfig { ClusterId = "orders", Destinations = new Dictionary<string, DestinationConfig> { ["orders-1"] = new() { Address = "https://orders.internal/" }, }, }]);
var app = builder.Build();app.UseAuthentication();app.UseAuthorization();app.MapReverseProxy(); // Authorization уходит в orders как есть, и orders проверяет его сноваapp.Run();- Почему бы просто не пробросить токен пользователя? — Нужен
audдля всех сервисов; токен, украденный в одном, работает везде. - Чем token exchange отличается от On-Behalf-Of в Entra ID? — OBO — вариант Microsoft для того же сценария: grant
jwt-bearerсrequested_token_use=on_behalf_of. - Что такое claim
act? — По RFC 8693 он указывает, кто действует от имениsub; цепочка вызовов — вложеннымиact. - Достаточно ли проверить токен на шлюзе? — Нет, если сервис доступен мимо шлюза; zero trust требует проверки в каждом сервисе.
- Что даёт mTLS поверх JWT? — Аутентификацию сервиса на транспорте и, по RFC 8705, привязку токена к сертификату.