Skip to content

Частина 2. Автентифікація і авторизація

🔰 Вступ

Минулого разу ми розглянули, як захистити канал зв’язку між клієнтом і сервером – зашифрувати дані, гарантувати їх цілісність і підтвердити автентичність за допомогою TLS. Здавалося б, левова частка роботи зроблена: дані зашифровані, підслухати або змінити їх практично неможливо, тож можна видихнути з полегшенням. Але чи справді цього достатньо?
TLS захищає канал зв’язку, але не дає відповідь на головне питання – а з ким саме розмовляє сервер?
info
Це питання частково вирішується за допомогою mTLS - двостороннього TLS-зʼєднання, де сервер перевіряє сертифікат клієнта. Однак це дуже затратно, негнучко, вимагає значних інфраструктурних затрат і зачасту - overkill.
Якщо зловмисник не може перехопити повідомлення, він усе одно може звернутися до сервера самостійно. І якщо сервер не здатен відрізнити справжнього клієнта від самозванця, він просто надасть доступ усім, хто “ввічливо” попросить.
Щоб уникнути цього, сервер повинен мати змогу:
ідентифікувати клієнта – однозначно розуміти, з ким він спілкується;
перевірити повноваження клієнта – розуміти, що саме дозволено робити цьому конкретному клієнтові.
Для цього і використовуються два фундаментальні механізми:
Автентифікація (authentication) – підтвердження особи користувача або сервісу. Вона дозволяє серверу точно знати, з ким він спілкується. Ніяких розмов із незнайомцями.
Авторизація (authorization) – перевірка дозволів клієнта. Після автентифікації сервер визначає, які саме дії користувач може виконувати, і яку інформацію має право запитувати.
Ця лекція присвячена тому, як реалізувати ці механізми у .NET-проектах. Ми розглянемо як базові варіанти автентифікації, так і сучасні сценарії з використанням JWT, OAuth 2.0, OpenID Connect, а також гнучкі моделі авторизації – на основі ролей, claims або політик.
Наша мета – не просто дати огляд механізмів, а навчитися:
обирати правильну стратегію для свого типу проекту;
уникати типових помилок при реалізації контролю доступу;
забезпечувати масштабованість і безпеку навіть у складних розподілених системах.
Почнемо з огляду основних методів автентифікації.

👋 Методи автентифікації

.NET надає гнучкий набір інструментів для автентифікації як у простих, так і в складних сценаріях – від класичного логіну з паролем до інтеграції з зовнішніми постачальниками (Azure AD, Google) та побудови мікросервісної автентифікації з використанням JWT і OAuth2.
Проте незалежно від конкретного підходу, суть усіх механізмів зводиться до передачі серверу (зазвичай в заголовках HTTP-запиту) параметру чи набору параметрів, за допомогою яких він має змогу однозначно ідентифікувати клієнта.
Нижче розглянемо ключові підходи.

*️⃣ Basic Authentication

Basic Authentication – один із найпростіших і найстаріших механізмів автентифікації. Суть полягає в тому, що клієнт передає свої облікові дані (логін і пароль) у кожному запиті до сервера. Ці дані кодуються в форматі base64 і надсилаються в заголовку Authorization.
Формат заголовка має такий вигляд:
Authorization: Basic dXNlcjpwYXNzd29yZA==
Це – ключове слово Basic + base64-кодоване значення username:password .

⚙️ Як це працює?

Клієнт (наприклад, браузер або Postman) формує запит до захищеного ресурсу.
У заголовку додається Authorization: Basic ....
Сервер перевіряє облікові дані:
Якщо вони правильні – виконується запит.
Якщо ні – повертається помилка 401 Unauthorized.
У випадку помилки, сервер зазвичай повертає заголовок:
WWW-Authenticate: Basic realm="<application realm>"
Це сигнал для клієнта, що потрібно повторити запит із правильними обліковими даними.

👍 Переваги

Простота реалізації – не потрібно налаштовувати сесії, бази даних токенів чи додаткову інфраструктуру.
Працює з будь-яким HTTP-клієнтом – підтримується "з коробки" в браузерах, Postman, curl, тощо.
Підходить для внутрішніх API – коли доступ має лише вузьке коло сервісів.

👎 Недоліки

Облікові дані передаються при кожному запиті – base64 не є шифруванням, воно легко розкодовується.
Відсутній механізм відкликання доступу – пароль або працює, або ні.
Немає розмежування сесій – неможливо легко відслідкувати активність користувача чи обмежити тривалість сесії.
Вразливість без HTTPS – якщо TLS не використовується, дані можуть бути перехоплені.
minus
Ніколи не використовуйте Basic Authentication без TLS!!!

#️⃣ Реалізація в .NET

Додайте NuGet-пакет
Створіть клас (наприклад, BasicAuthService), який імплементує інтерфейс IBasicUserValidationService
Цей інтерфейс містить лише один метод – валідацію логіна і пароля:
Task<bool> IsValidAsync(string username, string password)
В Program.cs додайте використання Basic Authentication, вкажіть Realm, а також підключіть ваш валідатор:
builder.Services.AddAuthentication(BasicDefaults.AuthenticationScheme)
.AddBasic<BasicAuthService>(options =>
{
options.Realm = "Products API";
});

builder.Services.AddAuthorization();

...
app.UseAuthentication();
app.UseAuthorization();
В контролері додайте атрибут Authorize над необхідним endpoint’ом - тепер до нього можна доступитися тільки за допомогою логіна і пароля:
[Authorize]
[HttpGet]
public IActionResult Get()
{
logger.LogInformation("Getting the list of products");
return Ok(Products);
}

⚛️ JWT (JSON Web Token)

JWT (JSON Web Token) – це самодостатній, підписаний токен, який використовується для автентифікації та передачі claims (відомостей про користувача) між клієнтом і сервером.
Він широко застосовується в REST API, SPAs, мікросервісах, а також при реалізації OAuth 2.0.
Як і у випадку Basic Authentication, при JWT автентифікації токен передається в заголовку авторизації, але з іншим ключовим словом - Bearer:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
JWT складається з трьох частин, розділених крапкою:
xxxxx.yyyyy.zzzzz
Заголовок (Header) – містить метадані про токен, як-то тип токена (JWT) та алгоритм підпису (наприклад, HS256, RS256)
Тіло (Payload) – несе інформацію про користувача та токен – так звані claims. Клейми бувають основні (стандартні) та користувацькі. Основні клейми бувають такі:
Claim
Опис
iss (issuer)
Хто видав токен (наприклад, your-app)
sub (subject)
Унікальний ідентифікатор суб’єкта (користувача або сервісу)
aud (audience)
Для кого призначений токен
exp (expiration)
Час закінчення дії токена (UNIX timestamp)
nbf (not before)
Час, раніше якого токен не є дійсним
iat (issued at)
Час створення токена
jti (JWT ID)
Унікальний ідентифікатор токена (може використовуватись для відкликання)
There are no rows in this table
Підпис (Signature) – підпис накладається на рядок, що включає base64 від заголовка та base64 від тіла, розділених крапкою.
error
Важливо! Хоча токен підписаний, його заголовок та тіло не зашифровані. Це звичайний base64, який легко перетворюється на читабельний рядок. Тому не варто передавати будь-яку чутливу інформацію в claims.

🔤 Види токенів

JWT-автентифікація працює з двома типами токенів.
Перший, токен доступу (access token), клієнт використовує для доступу до захищених ресурсів через API. Цей токен:
Містить claims (ID користувача, роль, права тощо)
Має короткий термін життя (5–30 хв)
Підписаний
Використовується у заголовку Authorization
Другий, токен оновлення (refresh token), застосовується лише для отримання нового токена доступу, коли попередній протермінувався. Цей вид токену:
Має триваліший строк дії (від декількох годин до декількох днів або і довше)
Передається лише з метою оновленні токена доступу
Зберігається безпечніше (більш чутливий до компрометування)
Зберігається на стороні сервера (для перевірки та відкликання)
info
Можливо реалізувати JWT-автентифікацію лише з використанням токена доступу, не використовуючи токен оновлення. В такому випадку якщо токен доступу протермінований, для отримання нового необхідно знову використати логін та пароль. Це не є безпечно, оскільки в такому разі логін і пароль (1) необхідно зберігати на клієнті і (2) щоразу передавати по мережі, збільшуючи ризик компрометування. Витік логіна і пароля значно критичніший за витік токена оновлення, оскільки токен оновлення з часом втрачає чинність, а також може бути відкликаний сервером.

⚙️ Як це працює?

Логін і пароль відправляється в запиті на автентифікацію. Як правило, це звичайний POST-запит у відкритий endpoint (наприклад, /login чи /token), в тілі якого передаються ці дані. Використання TLS - обовʼязкове.
Сервер перевіряє, чи в нього в базі даних є користувач з таким логіном, і чи правильно задано пароль. Важливо розуміти, що в базі даних зазвичай не зберігається пароль у відкритому вигляді. Перед зберіганням він хешується (наприклад, за допомогою bcrypt). Тому перед перевіркою даних автентифікації сервер хешує пароль, вказаний при логіні, і потім використовує цей хеш для перевірки.
Якщо користувач з таким логіном/паролем знайдений, сервер видає пару токенів – токен доступу (access token) та токен оновлення (refresh token). Токен доступу містить клейми sub, exp, name, role тощо, має короткий термін життя, зазвичай не зберігається в базі і використовується безпосередньо для автентифікації. Токен оновлення – довготривалий, зберігається в базі даних. Він використовується для отримання нового токена доступу після того, як старий втратив чинність. Токени підписуються симетричним (HS256) або асиметричним (RS256) ключем (токен оновлення може бути й без підпису) та відправляються у відповіді на запит автентифікації.
Клієнт отримує токени і зберігає їх в захищеному сховищі (залежно від типу додатку). При кожному запиті на сервер токен доступу додається в заголовок: Authorization: Bearer eyJhbGciOiJIUzI1NiIs....
Сервер перевіряє токен доступу за допомогою middleware – не потрібно в кожному enpoint явно додавати перевірку токена. Перевірка має такі етапи:
Парсимо токен
Перевіряємо підпис – за допомогою симетричного чи відкритого асиметричного ключа (залежно від алгоритму).
Перевіряєм валідність токена:
чи не протермінований токен (exp)?
чи правильний issuer та audience?
Якщо все добре – витягуємо клейми і переходимо до авторизації.
Якщо користувач має право доступу – запит виконується, якщо ні - сервер повертає помилку 401 Unauthorized або 403 Forbidden .
Якщо токен протерміновано, клієнт робить запит на оновлення. Як правило, це POST-запит у відкритий endpoint (наприклад, /refresh), в тілі якого передається токен оновлення. Є чимало стратегій оновлення токена доступу. Ось кілька з них:
По факту отримання 401 Unauthorized з повідомленням про те, що токен протерміновано.
на фоні з певним інтервалом, який менший за час життя токена.
Сервер перевіряє токен оновлення, надісланий клієнтом. Якщо все гаразд, видає новий токен доступу і (опціонально) оновлює refresh token. Сервер повертає клієнтові нові токени у відповідь на запит.
Клієнт замінює старі токени на нові у безпечному сховищі і використовує новий токен доступу для наступних запитів
image.png

👍 Переваги

Stateless – сервер не зберігає сесії, вся інформація “зашита” в токені
Висока масштабованість – працює добре в мікросервісах і кластеризованих середовищах (Kubernetes, хмара)
Швидка перевірка токена – не потрібно звертатися до БД, перевіряється підпис і клейми
Гнучкість claims – в токен можна “вшити” будь-які дані
Сумісність з OAuth 2.0 / OpenID Connect – це стандартизоване рішення, що інтегрується з Google, Microsoft, Facebook, IdentityServer та іншими

👎 Недоліки

Неможливість відкликати токен доступу – за замовчуванням токени доступу валідні до завершення терміну дії
Чутливість до краджіки токена – якщо токен доступу потрапить до зловмисника, він отримає повний доступ до системи
Відсутність шифрування за замовчуванням – токен підписується, але не шифрується – будь-хто може прочитати його вміст
Проблема з ротацією ключів сервера – при зміні ключів старі токени втрачають чинність
Зростання розміру запитів – JWT – доволі довгий рядок (особливо при використанні RS256)

#️⃣ Реалізація в .NET

Додайте пакет
Створіть клас, який буде відповідати за генерацію токенів (наприклад, JwtService). Нехай він має два методи:
public string GenerateAccessToken(string username) - створює токен доступу з такими клеймами:
name - імʼя користувача, передане в username
role - роль користувача (нехай буде Software Engineer)
Набір користувацьких клеймів:
department - відділ в компанії
company - компанія
birthDate - дата народження
public string GenerateRefreshToken()
Зареєструйте JwtService в DI - в Program.cs додайте рядок
builder.Services.AddSingleton<JwtService>();
Створіть контролер JWT-автентифікації, який буде давати можливість отримувати та оновлювати токени через API (наприклад, AuthController). Передайте JwtService в конструктор вашого контролера. Нехай він містить такі ендпоінти:
POST /auth/login :
перевіряє username і password з тіла запиту
У випадку успіху використовує JwtService для генерації токенів доступу та оновлення.
Зберігає токен оновлення для поточного username
Повертає access_token та refresh_token
POST /auth/refresh:
Перевіряє refreshToken з тіла запиту
Якщо такий токен існує в БД, перегенеровує токен доступу та оновлення за допомогою JwtService
Замінює старий токен оновлення на новий для знайденого username
Повертає новий access_token та refresh_token
Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.