Основи аутентифікації та авторизації
Основи аутентифікації та авторизації
1. Навіщо це потрібно?
Уявіть API інтернет-магазину. Ось лише кілька запитів, які він обробляє щоденно:
| Запит | Хто має право? |
|---|---|
GET /products | Будь-хто (навіть без логіну) |
POST /orders | Тільки зареєстрований покупець |
PUT /orders/42 | Тільки автор замовлення #42 |
DELETE /products/5 | Тільки адміністратор |
GET /admin/dashboard | Тільки адміністратор з правом ViewDashboard |
Без системи контролю доступу будь-який анонімний клієнт може видалити товари або переглянути адмін-панель. Система безпеки вирішує дві послідовні задачі:
🔑 Аутентифікація (Authentication)
🛡️ Авторизація (Authorization)
Аналогія: аеропорт
- Аутентифікація = перевірка паспорта на контролі. Охоронець визначає: ви — це ви. Паспорт = ваш токен. Якщо паспорта немає — вас не пустять далі.
- Авторизація = перевірка посадкового талона на гейті. Ви — відома особа (паспорт перевірено), але чи маєте ви право зайти на цей конкретний рейс? Посадковий талон = ваші дозволи (Claims).
2. Модель ідентичності .NET: Claims
Що таке Claim?
У .NET ідентичність користувача побудована на концепції Claims (тверджень). Claim — це факт про користувача у форматі «ключ-значення»:
| Claim (Type) | Value | Що означає |
|---|---|---|
ClaimTypes.Name | "Іван Петренко" | Ім'я користувача |
ClaimTypes.Email | "ivan@example.com" | |
ClaimTypes.Role | "Admin" | Роль |
"user_id" | "42" | Кастомний ID |
"subscription" | "premium" | Тип підписки |
Claims — це не тільки стандартні поля (ім'я, email). Ви можете створити будь-який claim: "department", "country", "age_verified". Це робить систему надзвичайно гнучкою.
Три рівні: Claim → ClaimsIdentity → ClaimsPrincipal
Ідентичність у .NET має трирівневу структуру:
Type (ключ) і Value (значення). Наприклад: new Claim(ClaimTypes.Name, "Іван").AuthenticationType — назву схеми, яка створила цю identity.HttpContext.User — саме із ним ви працюєте в ендпоінтах.Створення вручну
Щоб зрозуміти, як ці класи працюють, створимо їх вручну:
// 1. Створюємо набір тверджень (Claims)
var claims = new List<Claim>
{
new(ClaimTypes.Name, "Іван Петренко"),
new(ClaimTypes.Email, "ivan@example.com"),
new(ClaimTypes.Role, "Admin"),
new("user_id", "42"),
new("subscription", "premium")
};
// 2. Загортаємо у «посвідку» (Identity)
// "Bearer" — назва схеми аутентифікації
var identity = new ClaimsIdentity(claims, "Bearer");
// 3. Загортаємо у «користувача» (Principal)
var principal = new ClaimsPrincipal(identity);
// Тепер можемо перевірити:
Console.WriteLine(principal.Identity?.Name);
// → "Іван Петренко"
Console.WriteLine(
principal.Identity?.IsAuthenticated);
// → true (бо вказали AuthenticationType)
Console.WriteLine(
principal.IsInRole("Admin"));
// → true
Console.WriteLine(
principal.FindFirst("user_id")?.Value);
// → "42"
Зверніть увагу на другий аргумент конструктора ClaimsIdentity("Bearer"). Це AuthenticationType — назва схеми. Якщо не вказати — IsAuthenticated поверне false, і система вважатиме користувача анонімним, навіть якщо claims є!
ClaimsIdentity без AuthenticationType = анонімний користувач. Завжди вказуйте тип аутентифікації, навіть якщо він може бути будь-яким рядком.3. Потік аутентифікації у ASP.NET Core
Middleware Pipeline
Аутентифікація та авторизація — це два middleware у конвеєрі запитів. Їхній порядок критично важливий:
var builder = WebApplication.CreateBuilder(args);
// Реєстрація сервісів аутентифікації
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
var app = builder.Build();
// ⚠️ ПОРЯДОК КРИТИЧНИЙ!
app.UseAuthentication(); // 1. Хто ти?
app.UseAuthorization(); // 2. Чи маєш ти право?
app.MapGet("/secret", () => "Top Secret Data!")
.RequireAuthorization();
app.Run();
UseAuthentication() і UseAuthorization(), авторизація буде перевіряти анонімного користувача (бо аутентифікація ще не відбулася). Кожен захищений ендпоінт повертатиме 401.Що відбувається всередині?
Ось повний шлях HTTP-запиту через систему безпеки:
AuthenticationMiddleware
Middleware дивиться на запит і намагається ідентифікувати клієнта:
- Перевіряє, чи є заголовок
Authorization: Bearer <token>(або cookie, або інше джерело — залежить від схеми). - Якщо є — валідує токен (перевіряє підпис, срок дії, issuer тощо).
- Якщо валідний — створює
ClaimsPrincipalз claims, витягнутими з токена. - Зберігає його у
HttpContext.User. - Якщо токена немає або він невалідний —
HttpContext.Userзалишається анонімним (пустий principal).
Важливо: AuthenticationMiddleware не блокує запит! Навіть якщо токен невалідний — запит проходить далі. Блокування — задача AuthorizationMiddleware.
AuthorizationMiddleware
Middleware перевіряє, чи має HttpContext.User право на виконання запиту:
- Перевіряє, чи ендпоінт вимагає авторизації (
.RequireAuthorization()). - Якщо так — перевіряє
User.IsAuthenticated. Якщоfalse→401 Unauthorized. - Перевіряє ролі, політики, вимоги.
- Якщо вимоги не виконані →
403 Forbidden. - Якщо все ок — запит проходить до ендпоінту.
4. Схеми аутентифікації (Authentication Schemes)
Що таке схема?
ASP.NET Core підтримує множинну аутентифікацію. Один додаток може одночасно приймати:
- JWT-токени від мобільного додатка
- Cookies від веб-інтерфейсу
- API ключі від зовнішніх сервісів
Кожен такий спосіб аутентифікації називається схемою (Scheme). Схема — це іменована конфігурація, яка визначає:
- Як витягти дані з запиту (з заголовка? з cookie? з query string?)
- Як валідувати ці дані (перевірити підпис JWT? запитати базу?)
- Як створити ClaimsPrincipal з результату
builder.Services.AddAuthentication(options =>
{
// Схема за замовчуванням
options.DefaultScheme = "Cookies";
// Схема для Challenge (401)
options.DefaultChallengeScheme = "Bearer";
})
.AddJwtBearer("Bearer", options =>
{
// Конфігурація JWT...
})
.AddCookie("Cookies", options =>
{
// Конфігурація Cookie...
});
Вбудовані схеми ASP.NET Core
| Схема | NuGet-пакет | Для чого |
|---|---|---|
| JWT Bearer | Microsoft.AspNetCore.Authentication.JwtBearer | API-токени (stateless) |
| Cookie | Вбудований | Веб-додатки (stateful) |
| OAuth 2.0 | Microsoft.AspNetCore.Authentication.Google тощо | «Увійти через Google» |
| OpenID Connect | Microsoft.AspNetCore.Authentication.OpenIdConnect | Корпоративний SSO |
| Certificate | Вбудований | mTLS між сервісами |
У наступних статтях ми детально розглянемо кожну з них.
5. HttpContext.User — ваша точка доступу
Після проходження AuthenticationMiddleware, інформація про користувача доступна через HttpContext.User. Ось як працювати з ним у Minimal API:
app.MapGet("/me", (HttpContext ctx) =>
{
// Перевірка аутентифікації
if (ctx.User.Identity?.IsAuthenticated != true)
return Results.Json(
new { error = "Not authenticated" },
statusCode: 401);
// Читання Claims
var userId = ctx.User.FindFirst("user_id")?.Value;
var name = ctx.User.Identity.Name;
var email = ctx.User
.FindFirst(ClaimTypes.Email)?.Value;
var roles = ctx.User
.FindAll(ClaimTypes.Role)
.Select(c => c.Value)
.ToList();
return Results.Ok(new
{
userId,
name,
email,
roles,
isAdmin = ctx.User.IsInRole("Admin")
});
}).RequireAuthorization();
Зручний доступ через ClaimsPrincipal
ASP.NET Core дозволяє ін'єктувати ClaimsPrincipal напряму як параметр ендпоінту:
// Замість HttpContext — отримуємо User напряму
app.MapGet("/me", (ClaimsPrincipal user) =>
{
var name = user.Identity?.Name;
var email = user.FindFirst(ClaimTypes.Email)?.Value;
return Results.Ok(new { name, email });
}).RequireAuthorization();
Це коротший і чистіший варіант, який рекомендується для більшості випадків.
Типові помилки при роботі з Claims
// 💥 NullReferenceException, якщо claim відсутній!
var userId = int.Parse(
ctx.User.FindFirst("user_id").Value);
Виправлення:
// ✅ Безпечний варіант
var userIdClaim = ctx.User.FindFirst("user_id");
if (userIdClaim is null)
return Results.Unauthorized();
var userId = int.Parse(userIdClaim.Value);
// Створили identity БЕЗ типу аутентифікації
var identity = new ClaimsIdentity(claims);
// identity.IsAuthenticated == false! 😱
Виправлення:
// ✅ Завжди вказуйте AuthenticationType
var identity = new ClaimsIdentity(claims, "Bearer");
// identity.IsAuthenticated == true ✅
// ❌ Якщо у користувача 3 ролі —
// це не один claim з комою!
new Claim(ClaimTypes.Role, "Admin,Editor,User");
Виправлення:
// ✅ Кожна роль — окремий Claim
new Claim(ClaimTypes.Role, "Admin"),
new Claim(ClaimTypes.Role, "Editor"),
new Claim(ClaimTypes.Role, "User")
// user.IsInRole("Admin") → true ✅
6. RequireAuthorization та AllowAnonymous
Захист ендпоінтів
У Minimal API для захисту ендпоінту використовується метод .RequireAuthorization():
// 🔓 Публічний — без авторизації
app.MapGet("/products", () =>
Results.Ok(new[] { "Кава", "Чай" }));
// 🔒 Захищений — тільки для аутентифікованих
app.MapGet("/orders", (ClaimsPrincipal user) =>
Results.Ok($"Orders for {user.Identity?.Name}"))
.RequireAuthorization();
// 🔒 Захищений — тільки для ролі Admin
app.MapDelete("/products/{id}", (int id) =>
Results.NoContent())
.RequireAuthorization(policy =>
policy.RequireRole("Admin"));
Захист цілої групи
Через Route Groups можна захистити всі ендпоінти групи:
// Усе, що в /admin/* — тільки для Admin
var admin = app.MapGroup("/admin")
.RequireAuthorization(policy =>
policy.RequireRole("Admin"));
admin.MapGet("/users", () => "Users list");
admin.MapGet("/stats", () => "Statistics");
// Обидва ендпоінти захищені! ✅
AllowAnonymous — виняток із правила
Якщо група захищена, але один ендпоінт має бути публічним:
var api = app.MapGroup("/api")
.RequireAuthorization(); // Все захищено
api.MapGet("/products", () => "Products")
.AllowAnonymous(); // Крім цього! 🔓
api.MapPost("/orders", () => "Created");
// Цей — захищений 🔒
7. Мінімальний робочий приклад
Зібравши все разом, ось повний мінімальний додаток з аутентифікацією:
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
// Додаємо сервіси auth
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
var app = builder.Build();
// Порядок middleware!
app.UseAuthentication();
app.UseAuthorization();
// 🔓 Публічний ендпоінт
app.MapGet("/", () => "Welcome! Login at /login");
// ⚙️ «Логін» — створюємо ClaimsPrincipal вручну
// (у реальності тут буде JWT — наступна стаття)
app.MapGet("/login", () =>
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, "Іван"),
new(ClaimTypes.Role, "Admin"),
new("user_id", "42")
};
var identity = new ClaimsIdentity(claims, "Demo");
var principal = new ClaimsPrincipal(identity);
// Виводимо Claims для демонстрації
return Results.Ok(new
{
message = "Authenticated (demo)!",
name = principal.Identity?.Name,
isAuthenticated = principal
.Identity?.IsAuthenticated,
claims = claims.Select(c =>
new { c.Type, c.Value })
});
});
// 🔒 Захищений
app.MapGet("/me", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
name = user.Identity?.Name,
userId = user.FindFirst("user_id")?.Value,
isAdmin = user.IsInRole("Admin")
});
}).RequireAuthorization();
app.Run();
ClaimsPrincipal вручну, але не зберігаємо його між запитами. У реальності Claims надходять з JWT-токена (наступна стаття) або cookie (стаття 04). Поки що мета — зрозуміти, як працює модель Claims.8. Практичні завдання
Рівень 1: Базовий
Створіть ендпоінт GET /identity-info, який:
- Створює
ClaimsPrincipalвручну з 5 claims: Name, Email, Role (2 штуки — «User» і «Editor»), кастомнийdepartment= «IT» - Повертає JSON з усіма even claims (
Type+Value) - Повертає
IsAuthenticated,Name,IsInRole("Editor"),IsInRole("Admin") - Спробуйте створити
ClaimsIdentityбезAuthenticationType— що зміниться в результаті?
Створіть мінімальний додаток:
- Зареєструйте
AddAuthentication()таAddAuthorization() - Додайте 3 ендпоінти:
/public(🔓),/private(🔒.RequireAuthorization()),/admin(🔒.RequireRole("Admin")) - Протестуйте: що повертає кожен ендпоінт без аутентифікації? Який статус-код?
- Поміняйте місцями
UseAuthentication()таUseAuthorization()— що зміниться?
Рівень 2: Проєктування
Спроєктуйте API з трьома зонами доступу:
- Група
/api/public— без авторизації:GET /products,GET /categories - Група
/api/user— для аутентифікованих:GET /orders,POST /orders - Група
/api/admin— тільки роль Admin:GET /users,DELETE /users/{id} - В групі
/api/userодин ендпоінтGET /api/user/profileмає бути доступний без авторизації (.AllowAnonymous()) - Виведіть у відповіді кожного ендпоінту:
User.Identity.Name,IsAuthenticated, список ролей
Рівень 3: Архітектура
Створіть набір extension methods для зручної роботи з Claims:
GetUserId(this ClaimsPrincipal)→int?GetEmail(this ClaimsPrincipal)→string?HasRole(this ClaimsPrincipal, string role)→boolGetSubscriptionType(this ClaimsPrincipal)→string?(кастомний claim"subscription")- Використайте їх у 3 ендпоінтах замість прямого доступу до
FindFirst() - Чому extension methods краще, ніж прямі виклики
FindFirst? Яка перевага при рефакторингу?
9. Резюме
Authentication ≠ Authorization
Claims — основа всього
HttpContext.User
RequireAuthorization
Далі: у наступній статті ми реалізуємо повноцінну JWT-аутентифікацію — від генерації токенів до login/refresh ендпоінтів. Claims, які ми вивчили тут, стануть «начинкою» JWT-токена.
Процес проєктування API та документування
Покроковий алгоритм проєктування API: від аналізу предметної області до документації. OpenAPI/Swagger, версіонування, code style та чеклист якості.
JWT-аутентифікація
Повна реалізація JWT Bearer у Minimal API: генерація токенів, login/refresh ендпоінти, TokenValidationParameters, зберігання секретів, Access + Refresh Token стратегія.