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

OAuth 2.0 и OpenID Connect

Роли и потоки OAuth 2.0, токены и их жизнь, вход через OpenID Connect в ASP.NET Core и защита SPA. На этих протоколах держится корпоративный единый вход, поэтому на собеседовании проверяют и теорию, и опыт настройки.

Перед этой темой: Основы веб-безопасности.

Junior+

Что такое OAuth 2.0 и чем от него отличается OpenID Connect?

Заголовок раздела «Что такое OAuth 2.0 и чем от него отличается OpenID Connect?»
Ответ

Что это

  • OAuth 2.0 — протокол, по которому пользователь разрешает приложению доступ к своим данным, не отдавая ему пароль.
  • OpenID Connect — слой поверх OAuth 2.0, который добавляет вход: приложение узнаёт, кто пользователь.

Роли OAuth

Роль Пример
Resource owner — владелец ресурса пользователь
Client — клиент веб-приложение, SPA
Authorization server — сервер авторизации Keycloak, Entra ID, ADFS
Resource server — ресурсный сервер ваш Web API

Как пользоваться

  1. Клиент отправляет пользователя на сервер авторизации.
  2. Пользователь входит там и соглашается выдать доступ.
  3. Клиент получает access-токен и вызывает с ним API, а в OIDC — ещё и ID-токен.

Частая ошибка

Считать access-токен доказательством входа. Он адресован API, а кто вошёл, говорит ID-токен.

Как устроено

  • OAuth 2.0 (RFC 6749) описывает выдачу access-токена: endpoints /authorize и /token, типы клиентов и потоки (grant types). Формат токена и способ узнать пользователя он не задаёт.
  • Спецификации OpenID Connect добавляют поверх:
    • scope openid — признак OIDC-запроса, и стандартные profile, email;
    • ID-токен — всегда JWT с полями iss, sub, aud, exp, iat;
    • endpoint UserInfo — профиль пользователя по access-токену;
    • discovery-документ /.well-known/openid-configuration с адресами endpoints и ключей.

Когда что выбирать

Задача Что нужно
Войти в приложение под корпоративной учёткой OIDC: ID-токен и сессия приложения
Вызвать API от имени пользователя access-токен с нужным scope
Вызвать API от имени сервиса access-токен, выданный самому сервису

Ловушки

  • Вход по access-токену. Токен, выданный другому клиенту, подойдёт и вашему. ID-токен защищён от этого проверкой aud.
  • ID-токен в API. Отправлять его в заголовке Authorization — ошибка: его получатель — клиент, а не API.

Правило

Аутентификация пользователя — OIDC, доступ к API — access-токен с узким scope.

Под капотом

  • Сервер авторизации — единственное место, где пользователь вводит пароль или проходит Kerberos. Клиент видит только результат — токены.
  • API доверяет подписи сервера авторизации: проверяет издателя, аудиторию и срок токена. Подробно — в теме JWT и защита API.
  • ID-токен адресован клиенту: aud равен client_id, а nonce связывает ответ с конкретным запросом входа. Чужой или повторно присланный ID-токен клиент отвергнет.

Компромиссы

  • Делегирование против пароля в приложении. Пароль хранит один сервер, доступ отзывается по каждому клиенту отдельно. Цена — редиректы, ключи подписи и зависимость от доступности провайдера.
  • Единый вход (SSO). Сессия у сервера авторизации общая: вошёл в одно приложение — вошёл во все. Выход тоже приходится согласовывать: RP-initiated, front-channel и back-channel logout.
  • Протокол против продукта. OIDC стандартизирует обмен, но не модель прав: роли и группы каждый провайдер передаёт по-своему. ADFS, Entra ID и Keycloak различаются именами полей и поддержкой расширений.

Как проверить в проде

  • Discovery-документ провайдера показывает поддерживаемые потоки, scopes и алгоритмы: grant_types_supported, scopes_supported, id_token_signing_alg_values_supported.
  • Содержимое токенов смотрят в логах Development или локальным декодером, не отправляя боевые токены на сторонние сайты.

Итог: OAuth отделяет выдачу доступа от его использования, OIDC стандартизирует, кто вошёл, а держатся оба на доверии к подписи сервера авторизации.

Пример
// ID-токен после декодирования: адресован клиенту
{
"iss": "https://login.example.com", // кто выпустил
"sub": "248289761001", // постоянный id пользователя у провайдера
"aud": "orders-web", // client_id приложения
"exp": 1791374400, // срок действия, Unix time
"iat": 1791370800, // время выпуска
"nonce": "n-0S6_WzA2Mj", // связь с запросом входа
"name": "Olena Kovalenko"
}
// Access-токен того же входа (если провайдер выдаёт JWT): адресован API
{
"iss": "https://login.example.com",
"sub": "248289761001",
"aud": "https://api.example.com/orders", // получатель — API
"client_id": "orders-web", // кому выдан
"scope": "orders.read", // что клиенту разрешено
"exp": 1791371100
}
Middle+

Какие бывают потоки OAuth и какой когда выбрать?

Заголовок раздела «Какие бывают потоки OAuth и какой когда выбрать?»
Ответ

Что это

Поток (grant type) — сценарий, по которому клиент получает токен. Выбор зависит от двух вопросов: есть ли пользователь и может ли клиент хранить секрет.

Основные потоки

Поток Когда
Authorization code + PKCE входит пользователь: сайт, SPA, мобильное приложение
Client credentials сервис вызывает сервис, пользователя нет
Device code у устройства нет браузера: ТВ, консольная утилита
Refresh token продлить доступ без повторного входа

Частая ошибка

Взять поток с паролем (grant_type=password), потому что он «проще»: приложение само принимает логин и пароль. Тогда пароль видит клиент, а MFA и единый вход перестают работать. Современные рекомендации этот поток запрещают.

Как устроено: authorization code + PKCE

  1. Клиент генерирует случайный code_verifier и считает code_challenge — BASE64URL от его SHA-256.
  2. Браузер уходит на /authorize с response_type=code, client_id, redirect_uri, scope, state и code_challenge.
  3. Пользователь входит на сервере авторизации и подтверждает доступ.
  4. Сервер авторизации возвращает браузер на redirect_uri с одноразовым code и тем же state.
  5. Клиент сверяет state и отправляет POST на /token с code и code_verifier; конфиденциальный клиент добавляет свой секрет.
  6. Сервер проверяет, что хэш code_verifier совпал с code_challenge, и выдаёт токены.

Когда что выбирать

  • Конфиденциальный клиент — серверное веб-приложение или сервис с секретом: authorization code + PKCE для пользователей, client credentials для вызовов без пользователя.
  • Публичный клиент — SPA, мобильное или десктопное приложение: секрета нет, PKCE обязателен.

Ловушки

  • Implicit (response_type=token) кладёт access-токен во фрагмент URL: он попадает в историю браузера, его можно подменить.
  • Нестрогий redirect_uri. Сравнение по префиксу или маске позволяет увести код на подставной адрес. RFC 9700 требует точного совпадения строк.

Правило

Authorization code + PKCE для всех клиентов с пользователем, client credentials для сервисов, device code для устройств без браузера. Implicit и password в новом коде не используют.

Под капотом

  • PKCE защищает от перехвата и подмены кода: украденный code бесполезен без code_verifier, который не покидал клиента. Метод — S256; plain в OAuth 2.1 убран.
  • Фронт-канал (редиректы браузера) ненадёжен: URL видят история, расширения и логи. Поэтому по нему идёт только короткоживущий одноразовый код, а токены — прямым ответом по TLS.
  • Device code (RFC 8628): устройство получает device_code и user_code, показывает адрес и код, пользователь входит с телефона, а устройство опрашивает /token.
  • Client credentials выдаёт токен самому клиенту, и refresh-токен в нём не нужен: RFC 6749 советует его не выдавать.

Статус стандартов

  • RFC 9700 (OAuth 2.0 Security BCP, январь 2025): password — MUST NOT, implicit — SHOULD NOT, PKCE обязателен публичным клиентам и рекомендован конфиденциальным.
  • OAuth 2.1 на октябрь 2026 — ещё черновик IETF. В нём нет implicit и password, а PKCE обязателен всем, кроме конфиденциального клиента с проверкой OIDC nonce.

Компромиссы

Сервису, которому нужен пользователь, client credentials не подходит: токен будет от имени сервиса. Тогда меняют токен пользователя на новый: token exchange (RFC 8693) или on-behalf-of в Entra ID.

Как проверить в проде

  • В discovery-документе — grant_types_supported и code_challenge_methods_supported с S256; у клиента в настройках провайдера выключены implicit и password.
  • В логах прокси и веб-сервера не должно быть access_token= в URL.

Итог: выбор потока — это выбор канала для токена и способа доказать, что клиент тот же; стандарты оставили authorization code + PKCE, client credentials и device code.

Authorization code + PKCE по HTTP
GET /authorize?response_type=code&client_id=orders-web
&redirect_uri=https%3A%2F%2Forders.example.com%2Fsignin-oidc
&scope=openid%20profile%20orders.read&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: login.example.com
POST /token HTTP/1.1
Host: login.example.com
Authorization: Basic b3JkZXJzLXdlYjpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Forders.example.com%2Fsignin-oidc
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Строка запроса разбита для читаемости. Первый запрос идёт через браузер, второй — напрямую от сервера приложения; заголовок Authorization: Basic есть только у конфиденциального клиента.

Как получить code_verifier и code_challenge
using System.Buffers.Text;
using System.Security.Cryptography;
using System.Text;
// code_verifier: 32 случайных байта → 43 символа base64url
var verifier = Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(32));
// code_challenge = BASE64URL(SHA256(code_verifier)), метод S256
var challenge = Base64Url.EncodeToString(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
Console.WriteLine(verifier.Length); // 43
Console.WriteLine(challenge.Length); // 43
Middle+

Какие бывают токены и как управлять их жизнью?

Заголовок раздела «Какие бывают токены и как управлять их жизнью?»
Ответ

Что это

Токен Кому адресован Зачем
Access API вызвать API
Refresh серверу авторизации получить новый access-токен
ID (OIDC) клиенту узнать, кто вошёл

Как пользоваться

  • Access-токен отправляют в заголовке Authorization: Bearer .... Это bearer-токен: кто им владеет, тот и пользуется.
  • Когда он истекает, клиент отправляет refresh-токен на /token и получает новую пару.
  • Refresh-токен выдают по scope offline_access (OIDC) или по настройке клиента.

Частая ошибка

Выдавать access-токен на сутки, чтобы «не возиться с обновлением». Украденный токен будет работать сутки, и если это JWT, отозвать его нельзя.

Как устроено

  • Scope — права клиента, а не пользователя: orders.read значит «клиент может читать заказы от имени пользователя». Итоговый доступ — пересечение прав пользователя и выданных scopes.
  • Audience (aud) — для какого API токен. API обязан отклонить токен с чужой аудиторией, иначе токен одного сервиса подойдёт к другому.
  • Время жизни — expires_in в ответе и exp в токене: минуты у access-токена, часы или дни у refresh-токена.

Когда что выбирать

Признак JWT Непрозрачный
Проверка локально, по подписи introspection (RFC 7662)
Отзыв только истечением срока мгновенно
Нагрузка на сервер авторизации нет на каждый запрос или промах кеша

Структура и проверка JWT — в теме JWT и защита API.

Ловушки

  • Параллельное обновление. Две вкладки одновременно обновляют токен одним refresh-токеном: при ротации вторая получит invalid_grant. Обновление на клиенте выполняют по одному.
  • Выход без отзыва. Удалить токены у себя недостаточно: refresh-токен действует, пока его не отозвали на revocation endpoint (RFC 7009).

Правило

Короткий access-токен с точной audience, refresh-токен с ротацией и отзыв при выходе.

Под капотом

  • Ротация (RFC 9700): каждый ответ на grant_type=refresh_token содержит новый refresh-токен, а старый становится недействительным. Если старый придёт снова, сервер не знает, кто из двоих атакующий, и отзывает всю цепочку.
  • Отзыв (RFC 7009): отозвав refresh-токен, сервер должен отозвать и access-токены того же гранта, если умеет. Уже выданный JWT API продолжит принимать до exp.
  • Introspection (RFC 7662): ответ {"active": true} со scope, клиентом и сроком или {"active": false} для отозванного и истёкшего токена.
  • Привязка к отправителю. Refresh-токен конфиденциального клиента бесполезен без его секрета; публичному клиенту привязку даёт DPoP (RFC 9449).

Цена

  • Короткий access-токен — регулярные обновления от каждого пользователя и нагрузка на сервер авторизации.
  • Introspection добавляет сетевой вызов к запросам API, а кеш его результата снова откладывает отзыв.

Компромиссы

  • Срок access-токена — окно, в котором отозванный доступ ещё работает.
  • Срок refresh-токена задаёт, как часто пользователь входит заново: абсолютный или скользящий, продлеваемый при использовании.
  • Новые права пользователя попадают в токен только при обновлении.

Как проверить в проде

  • Рост invalid_grant при обновлении в логах сервера авторизации — гонки обновления или обнаруженный повтор.
  • Тестами проверяют, что API отклоняет истёкший токен и токен с чужой aud.

Итог: короткий access-токен ограничивает ущерб, а управление сессией переносится на refresh-токен с ротацией, сроком и отзывом.

Обновление с ротацией
POST /token HTTP/1.1
Host: login.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&client_id=orders-spa
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 300, // 5 минут
"refresh_token": "8xLOxBtZp8RmMKb7wWUcAw", // новый: прежний больше не действует
"scope": "openid orders.read"
}
Отзыв при выходе
POST /revoke HTTP/1.1
Host: login.example.com
Content-Type: application/x-www-form-urlencoded
token=8xLOxBtZp8RmMKb7wWUcAw&token_type_hint=refresh_token&client_id=orders-spa

Адреса /token, /revoke и /introspect у каждого провайдера свои: их берут из discovery-документа.

Middle+

Как подключить вход через OpenID Connect в ASP.NET Core?

Заголовок раздела «Как подключить вход через OpenID Connect в ASP.NET Core?»
Ответ

Что это

Вход через OIDC в ASP.NET Core — две схемы аутентификации, которые работают вместе:

  • OpenID Connect (пакет Microsoft.AspNetCore.Authentication.OpenIdConnect) ведёт пользователя к провайдеру и разбирает ответ;
  • Cookie запоминает вход между запросами.

Как пользоваться

  1. Зарегистрируйте приложение у провайдера: client_id, секрет, redirect URI https://ваш-сайт/signin-oidc.
  2. В Program.cs вызовите AddAuthentication со схемой по умолчанию cookie и схемой challenge OIDC, затем AddCookie() и AddOpenIdConnect(...).
  3. В AddOpenIdConnect укажите Authority, ClientId, ClientSecret и ResponseType = "code".
  4. Защитите эндпоинты через RequireAuthorization() или [Authorize].

Частая ошибка

Хранить ClientSecret в appsettings.json в репозитории. При разработке секрет кладут в user secrets, в проде — в хранилище секретов.

Как устроено

  1. Анонимный запрос к защищённому эндпоинту вызывает challenge схемы OIDC.
  2. Обработчик читает и кеширует discovery-документ провайдера и редиректит на /authorize с state, nonce и PKCE.
  3. Браузер возвращается с кодом POST-запросом на /signin-oidc: по умолчанию response_mode=form_post.
  4. Обработчик меняет код на токены и проверяет у ID-токена подпись, iss, aud, срок и nonce.
  5. Cookie-схема выдаёт cookie, и браузер возвращается на исходный адрес.

Настройки по умолчанию

Настройка Умолчание Когда менять
ResponseType id_token всегда на code
UsePkce true не трогать
SaveTokens false нужен access-токен для API
MapInboundClaims true нужны исходные имена claims
GetClaimsFromUserInfoEndpoint false в ID-токене мало данных

Ловушки

  • Переименование claims. При MapInboundClaims = true claim sub превращается в http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier, и FindFirst("sub") вернёт null. При false задайте NameClaimType и RoleClaimType.
  • Нет обновления токена. Обработчик не обновляет access-токен: по refresh-токену это делает ваш код или библиотека.
  • Reverse proxy. Без forwarded headers redirect URI собирается со схемой http, и провайдер его отвергает.

Правило

ResponseType = code, секрет вне репозитория, MapInboundClaims = false с явными NameClaimType и RoleClaimType.

Под капотом

  • state — зашифрованные AuthenticationProperties: адрес возврата и code_verifier уходят к провайдеру и возвращаются обратно. Correlation-cookie проверяет, что колбэк пришёл в тот же браузер.
  • Correlation- и nonce-cookie выставляются с SameSite=None; Secure: колбэк приходит межсайтовым POST, и с Lax браузер их не отправит.
  • Ключи подписи берутся из jwks_uri; при незнакомом kid обработчик перечитывает метаданные (RefreshOnIssuerKeyNotFound).
  • PAR (.NET 9+, PushedAuthorizationBehavior.UseIfAvailable): параметры уходят прямым POST, браузер получает только request_uri. Entra ID PAR не поддерживает.

Цена

SaveTokens = true кладёт токены в cookie: несколько килобайт, которые делятся на части и уходят с каждым запросом. SessionStore (ITicketStore) переносит сессию в кеш, в cookie остаётся ключ.

Компромиссы

  • Срок сессии задаёт cookie (ExpireTimeSpan), а не ID-токен.
  • Обновление access-токена пишут в OnValidatePrincipal cookie-схемы или берут библиотеку вроде Duende.AccessTokenManagement.
  • Для Entra ID Microsoft рекомендует Microsoft.Identity.Web; ADFS работает со стандартным обработчиком — подробнее в теме «AD, Kerberos и ADFS».

Как проверить в проде

  • Correlation failed — потерялась correlation-cookie: SameSite или HTTP за прокси.
  • Unable to unprotect the message.State — колбэк попал на узел с другими ключами Data Protection.
  • IdentityModelEventSource.ShowPII = true — только в Development: иначе детали ошибок валидации скрыты.

Итог: OIDC-обработчик — конечный автомат поверх cookie; надёжность входа решают ключи Data Protection, SameSite, прокси и размер сессии.

Пример
Microsoft.AspNetCore.Authentication.OpenIdConnect
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; // сессия
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; // вход
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.example.com"; // отсюда — discovery-документ
options.ClientId = "orders-web";
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code; // по умолчанию id_token
options.Scope.Add("orders.read"); // openid и profile уже есть
options.SaveTokens = true; // токены — в cookie сессии
options.MapInboundClaims = false; // sub остаётся sub
options.TokenValidationParameters.NameClaimType = "name";
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", async (HttpContext ctx) => new
{
name = ctx.User.Identity?.Name,
sub = ctx.User.FindFirst("sub")?.Value,
hasAccessToken = await ctx.GetTokenAsync("access_token") is not null
}).RequireAuthorization(); // анонимного пользователя отправит к провайдеру
// Выход из обеих схем: удалить cookie и завершить сессию у провайдера
app.MapGet("/logout", () => Results.SignOut(null,
[CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));
app.Run();
Senior+

Как безопасно авторизовать SPA: токены в браузере или BFF?

Заголовок раздела «Как безопасно авторизовать SPA: токены в браузере или BFF?»
Ответ

Что это

У SPA нет надёжного места для секрета: весь её код и данные доступны в браузере. Подходов два:

  • Токены в браузере. SPA сама проходит authorization code + PKCE, например через angular-oauth2-oidc, и отправляет access-токен в API.
  • BFF (Backend for Frontend). Вход выполняет сервер приложения, токены остаются на сервере, а браузер получает только cookie.

Зачем

Если на страницу попал чужой скрипт (XSS), он прочитает токен из localStorage и отправит атакующему. HttpOnly-cookie скрипт прочитать не может.

Частая ошибка

Думать, что sessionStorage вместо localStorage решает проблему. Скрипт на странице читает и то и другое.

Как устроено: BFF

  1. SPA и BFF живут на одном сайте, например app.example.com и app.example.com/bff.
  2. Вход — обычный OIDC-вход серверного приложения: BFF — конфиденциальный клиент с секретом.
  3. BFF выдаёт сессионную cookie: HttpOnly, Secure, SameSite и префикс __Host-.
  4. SPA вызывает /bff/... с cookie, а BFF берёт access-токен из сессии, при необходимости обновляет его и проксирует запрос в API.

Когда что выбирать

Критерий Токены в браузере BFF
Кража токена при XSS возможна токенов в браузере нет
Инфраструктура только статика сервер и прокси для API
CSRF к bearer-токену неприменим нужна защита

Ловушки

  • Silent renew через iframe опирается на cookie провайдера в стороннем контексте. Safari блокирует сторонние cookie, Firefox изолирует их по сайтам, поэтому без BFF обновляют refresh-токеном. angular-oauth2-oidc в code flow так и делает, а iframe включается useSilentRefresh: true.
  • Refresh-токен в браузере — самая ценная добыча. RFC 9700 требует для публичных клиентов ротацию или привязку к ключу.
  • CSRF в BFF. Cookie прикладывается сама, поэтому эндпоинты BFF требуют свой заголовок, например X-CSRF: 1: чужой сайт не отправит его без разрешения CORS.

Правило

Корпоративное приложение с персональными данными — BFF. Токены в браузере — когда сервера нет и риск приемлем: PKCE, короткие токены, ротация refresh-токенов.

Под капотом

  • RFC 10017 разбирает атаку, против которой хранилище бессильно. Внедрённый скрипт открывает скрытый iframe с /authorize?prompt=none, читает код из фрейма своего origin и меняет его на свежие токены.
  • DPoP (RFC 9449) привязывает токен к ключу клиента: каждый запрос подписан доказательством владения ключом. Неизвлекаемый ключ WebCrypto не даёт унести токен, но скрипт на странице может подписывать запросы сам или пройти новый вход со своим ключом.
  • Token-mediating backend получает токены на сервере, а access-токен отдаёт браузеру: refresh-токен не утекает, access-токен может.

Цена

  • BFF стоит на пути каждого вызова API: задержка прокси, своя доступность и масштабирование.
  • Сессия с токенами раздувает cookie, а SessionStore требует общего кеша между узлами.

Компромиссы

  • Cookie возвращает CSRF, от которого bearer-токены были свободны: нужны SameSite и свой заголовок.
  • SameSite=Strict RFC 10017 рекомендует, но после межсайтового колбэка OIDC первый запрос может прийти без cookie. В ASP.NET Core обычно оставляют Lax.
  • Готовые реализации: Duende BFF, YARP с подстановкой токена, пример Microsoft для Blazor Web App с OIDC.

Как объяснить решение команде

  • Разобрать модель угроз: что получит атакующий при XSS в каждом варианте.
  • Если ASP.NET Core уже отдаёт SPA, BFF обходится в несколько эндпоинтов и прокси.

Итог: XSS в SPA не лечится выбором хранилища; BFF не отдаёт токены браузеру и сужает ущерб до открытой вкладки.

BFF на ASP.NET Core
Microsoft.AspNetCore.Authentication.OpenIdConnect
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "__Host-orders"; // Secure, Path=/, без Domain
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HttpOnly включён по умолчанию
options.Cookie.SameSite = SameSiteMode.Lax; // Strict ломает возврат после входа
})
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.example.com";
options.ClientId = "orders-bff"; // конфиденциальный клиент
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true; // токены — в зашифрованной cookie, JavaScript их не видит
});
builder.Services.AddAuthorization();
builder.Services.AddHttpClient("api", c => c.BaseAddress = new Uri("https://api.example.com"));
var app = builder.Build();
// SPA вызывает /bff/orders с cookie, BFF добавляет токен и идёт в API
app.MapGet("/bff/orders", async (HttpContext ctx, IHttpClientFactory http) =>
{
if (ctx.Request.Headers["X-CSRF"] != "1") return Results.BadRequest(); // защита от CSRF
using var request = new HttpRequestMessage(HttpMethod.Get, "/orders");
request.Headers.Authorization = new("Bearer", await ctx.GetTokenAsync("access_token"));
using var response = await http.CreateClient("api").SendAsync(request);
return Results.Text(await response.Content.ReadAsStringAsync(), "application/json",
statusCode: (int)response.StatusCode);
}).RequireAuthorization();
app.Run();
Токены в браузере: angular-oauth2-oidc
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'https://login.example.com',
clientId: 'orders-spa', // публичный клиент: секрета нет
redirectUri: window.location.origin + '/index.html',
responseType: 'code', // authorization code + PKCE
scope: 'openid profile orders.read offline_access', // offline_access — за refresh-токеном
};
export async function initAuth(oauth: OAuthService) {
oauth.configure(authConfig);
await oauth.loadDiscoveryDocumentAndLogin();
oauth.setupAutomaticSilentRefresh(); // обновит токен на 75% его срока
}

Токены здесь лежат в sessionStorage — хранилище библиотеки по умолчанию.