Auth

Основи аутентифікації та авторизації

Authentication vs Authorization, Claims-модель ідентичності .NET, ClaimsPrincipal, AuthenticationMiddleware, схеми аутентифікації та повний потік обробки запиту.

Основи аутентифікації та авторизації

За даними OWASP Top 10, «Broken Access Control» — це вразливість #1 у веб-додатках. Понад 94% додатків мають ту чи іншу проблему з контролем доступу. У цій статті ми розберемо фундамент, на якому будується весь захист ASP.NET Core: як фреймворк дізнається хто робить запит і чи має він право це робити.

1. Навіщо це потрібно?

Уявіть API інтернет-магазину. Ось лише кілька запитів, які він обробляє щоденно:

ЗапитХто має право?
GET /productsБудь-хто (навіть без логіну)
POST /ordersТільки зареєстрований покупець
PUT /orders/42Тільки автор замовлення #42
DELETE /products/5Тільки адміністратор
GET /admin/dashboardТільки адміністратор з правом ViewDashboard

Без системи контролю доступу будь-який анонімний клієнт може видалити товари або переглянути адмін-панель. Система безпеки вирішує дві послідовні задачі:

🔑 Аутентифікація (Authentication)

«Хто ти?» — перевірка ідентичності. Клієнт надає доказ того, хто він (логін/пароль, токен, cookie). Результат: система знає ім'я, ID та роль користувача — або знає, що запит анонімний.

🛡️ Авторизація (Authorization)

«Що тобі дозволено?» — перевірка прав. Вже знаючи хто робить запит, система перевіряє: чи має цей користувач дозвіл на конкретну дію з конкретним ресурсом.
Порядок завжди фіксований: спочатку аутентифікація, потім авторизація. Не можна перевірити права, не знаючи, хто робить запит. Це відображено в порядку middleware у ASP.NET Core.

Аналогія: аеропорт

  • Аутентифікація = перевірка паспорта на контролі. Охоронець визначає: ви — це ви. Паспорт = ваш токен. Якщо паспорта немає — вас не пустять далі.
  • Авторизація = перевірка посадкового талона на гейті. Ви — відома особа (паспорт перевірено), але чи маєте ви право зайти на цей конкретний рейс? Посадковий талон = ваші дозволи (Claims).

2. Модель ідентичності .NET: Claims

Що таке Claim?

У .NET ідентичність користувача побудована на концепції Claims (тверджень). Claim — це факт про користувача у форматі «ключ-значення»:

Claim (Type)ValueЩо означає
ClaimTypes.Name"Іван Петренко"Ім'я користувача
ClaimTypes.Email"ivan@example.com"Email
ClaimTypes.Role"Admin"Роль
"user_id""42"Кастомний ID
"subscription""premium"Тип підписки

Claims — це не тільки стандартні поля (ім'я, email). Ви можете створити будь-який claim: "department", "country", "age_verified". Це робить систему надзвичайно гнучкою.

Три рівні: Claim → ClaimsIdentity → ClaimsPrincipal

Ідентичність у .NET має трирівневу структуру:

Loading diagram...
graph TD
    A["👤 ClaimsPrincipal<br/>(Користувач)"] --> B["🪪 ClaimsIdentity #1<br/>(JWT Bearer)"]
    A --> C["🪪 ClaimsIdentity #2<br/>(Cookie)"]
    B --> D["📋 Claim: Name = Іван"]
    B --> E["📋 Claim: Role = Admin"]
    B --> F["📋 Claim: Email = ivan@mail.com"]
    C --> G["📋 Claim: SessionId = abc123"]
    C --> H["📋 Claim: LoginProvider = Google"]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#64748b,stroke:#334155,color:#ffffff
    style E fill:#64748b,stroke:#334155,color:#ffffff
    style F fill:#64748b,stroke:#334155,color:#ffffff
    style G fill:#64748b,stroke:#334155,color:#ffffff
    style H fill:#64748b,stroke:#334155,color:#ffffff
Claim
record
Один факт про користувача. Має Type (ключ) і Value (значення). Наприклад: new Claim(ClaimTypes.Name, "Іван").
ClaimsIdentity
class
Колекція Claims, отримана з одного джерела (одна «посвідка»). Наприклад: identity від JWT-токена або identity від Google OAuth. Має властивість AuthenticationType — назву схеми, яка створила цю identity.
ClaimsPrincipal
class
Самий користувач. Може мати декілька ClaimsIdentity одночасно (JWT + Cookie). Це HttpContext.User — саме із ним ви працюєте в ендпоінтах.

Створення вручну

Щоб зрозуміти, як ці класи працюють, створимо їх вручну:

Створення ClaimsPrincipal вручну
// 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 у конвеєрі запитів. Їхній порядок критично важливий:

Program.cs — порядок 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-запиту через систему безпеки:

Loading diagram...
sequenceDiagram
    participant C as 📱 Клієнт
    participant AM as 🔑 AuthenticationMiddleware
    participant AZ as 🛡️ AuthorizationMiddleware
    participant E as 🎯 Endpoint

    C->>AM: GET /secret<br/>Authorization: Bearer eyJhbG...

    Note over AM: 1. Витягує токен з заголовка<br/>2. Валідує підпис та термін дії<br/>3. Створює ClaimsPrincipal<br/>4. Зберігає у HttpContext.User

    AM->>AZ: HttpContext.User заповнений

    Note over AZ: 1. Перевіряє RequireAuthorization()<br/>2. User.IsAuthenticated == true?<br/>3. Перевіряє ролі/політики

    AZ->>E: Авторизація пройшла ✅

    E-->>C: 200 OK: "Top Secret Data!"

AuthenticationMiddleware

Middleware дивиться на запит і намагається ідентифікувати клієнта:

  1. Перевіряє, чи є заголовок Authorization: Bearer <token> (або cookie, або інше джерело — залежить від схеми).
  2. Якщо є — валідує токен (перевіряє підпис, срок дії, issuer тощо).
  3. Якщо валідний — створює ClaimsPrincipal з claims, витягнутими з токена.
  4. Зберігає його у HttpContext.User.
  5. Якщо токена немає або він невалідний — HttpContext.User залишається анонімним (пустий principal).

Важливо: AuthenticationMiddleware не блокує запит! Навіть якщо токен невалідний — запит проходить далі. Блокування — задача AuthorizationMiddleware.

AuthorizationMiddleware

Middleware перевіряє, чи має HttpContext.User право на виконання запиту:

  1. Перевіряє, чи ендпоінт вимагає авторизації (.RequireAuthorization()).
  2. Якщо так — перевіряє User.IsAuthenticated. Якщо false401 Unauthorized.
  3. Перевіряє ролі, політики, вимоги.
  4. Якщо вимоги не виконані → 403 Forbidden.
  5. Якщо все ок — запит проходить до ендпоінту.

4. Схеми аутентифікації (Authentication Schemes)

Що таке схема?

ASP.NET Core підтримує множинну аутентифікацію. Один додаток може одночасно приймати:

  • JWT-токени від мобільного додатка
  • Cookies від веб-інтерфейсу
  • API ключі від зовнішніх сервісів

Кожен такий спосіб аутентифікації називається схемою (Scheme). Схема — це іменована конфігурація, яка визначає:

  1. Як витягти дані з запиту (з заголовка? з cookie? з query string?)
  2. Як валідувати ці дані (перевірити підпис JWT? запитати базу?)
  3. Як створити 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 BearerMicrosoft.AspNetCore.Authentication.JwtBearerAPI-токени (stateless)
CookieВбудованийВеб-додатки (stateful)
OAuth 2.0Microsoft.AspNetCore.Authentication.Google тощо«Увійти через Google»
OpenID ConnectMicrosoft.AspNetCore.Authentication.OpenIdConnectКорпоративний SSO
CertificateВбудованийmTLS між сервісами

У наступних статтях ми детально розглянемо кожну з них.


5. HttpContext.User — ваша точка доступу

Після проходження AuthenticationMiddleware, інформація про користувача доступна через HttpContext.User. Ось як працювати з ним у Minimal API:

Доступ до Claims у ендпоінті
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 напряму як параметр ендпоінту:

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


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 можна захистити всі ендпоінти групи:

Route Group із авторизацією
// Усе, що в /admin/* — тільки для Admin
var admin = app.MapGroup("/admin")
    .RequireAuthorization(policy =>
        policy.RequireRole("Admin"));

admin.MapGet("/users", () => "Users list");
admin.MapGet("/stats", () => "Statistics");
// Обидва ендпоінти захищені! ✅

AllowAnonymous — виняток із правила

Якщо група захищена, але один ендпоінт має бути публічним:

AllowAnonymous — виключення
var api = app.MapGroup("/api")
    .RequireAuthorization();  // Все захищено

api.MapGet("/products", () => "Products")
    .AllowAnonymous();  // Крім цього! 🔓

api.MapPost("/orders", () => "Created");
// Цей — захищений 🔒

7. Мінімальний робочий приклад

Зібравши все разом, ось повний мінімальний додаток з аутентифікацією:

Program.cs — мінімальний auth
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: Базовий

Рівень 2: Проєктування

Рівень 3: Архітектура


9. Резюме

Authentication ≠ Authorization

Аутентифікація — «хто ти?». Авторизація — «що тобі дозволено?». Спочатку перша, потім друга. Порядок middleware критичний.

Claims — основа всього

Claim → ClaimsIdentity → ClaimsPrincipal. Кожен факт про користувача — окремий Claim. Не забувайте AuthenticationType!

HttpContext.User

ClaimsPrincipal доступний через HttpContext.User або як параметр ендпоінту. FindFirst для отримання claims, IsInRole для перевірки ролей.

RequireAuthorization

Захист ендпоінтів — через .RequireAuthorization(). Route Groups — для масового захисту. AllowAnonymous — для виключень.

Далі: у наступній статті ми реалізуємо повноцінну JWT-аутентифікацію — від генерації токенів до login/refresh ендпоінтів. Claims, які ми вивчили тут, стануть «начинкою» JWT-токена.

Copyright © 2026