Перейти до вмісту

OAuth 2.0 і OpenID Connect

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

Перед цією темою: Основи вебзахисту.

Junior+

Що таке OAuth 2.0 і чим від нього відрізняється OpenID Connect?

Section titled “Що таке 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 і який коли обрати?

Section titled “Які бувають потоки 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+

Які бувають токени і як керувати їхнім життям?

Section titled “Які бувають токени і як керувати їхнім життям?”
Відповідь

Що це

Токен Кому адресований Навіщо
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?

Section titled “Як підключити вхід через 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?

Section titled “Як безпечно авторизувати 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 — сховищі бібліотеки за замовчуванням.