Аутентифікація проти авторизації: де кожна з них має бути у вашому коді
Дізнайтеся, як різняться процеси автентифікації та авторизації за способом реалізації, кодами стану HTTP та архітектурою системи, щоб уникнути поширених проблем з безпекою.
Два терміни, які здаються майже взаємозамінними при поверхневому огляді, проте кожен з них керує абсолютно різною частиною логіки вашої системи.
Якщо ви коли-небудь повертали код 401 у ситуації, коли насправді потрібен був код 403, або реалізовували процес входу та оголошували, що автентифікація завершена, не торкаючись при цьому логіки дозволів, ви вже стикалися з проблемами, які виникають від плутанини цих двох понять. Давайте остаточно розібратися в них.
Автентифікація: перевірка ідентичності
Автентифікація — це етап, під час якого система підтверджує, що користувач справді є тим, за кого себе видає. Вона відповідає на одне запитання: чи є ця ідентичність достовірною?
Це одноразова подія, яка зазвичай відбувається під час входу, і її результатом є певний тип облікових даних — сеанс, токен чи куки — які дозволяють решті програми довіряти користувачеві без необхідності повторно перевіряти пароль при кожному запиті.
Типові способи реалізації автентифікації включають:
- Автентифікація на основі пароля — надані облікові дані порівнюються з хешованою версією, яка зберігається
- Автентифікація на основі токена (JWT) — після успішного входу генерується підписаний токен, який потім перевіряється при кожному наступному запиті
- OAuth 2.0 / OpenID Connect — перевірка ідентичності передається зовнішньому постачальнику, такому як Google чи GitHub
- Ключі API — фіксований секретний рядок, який ідентифікує певного клієнта чи сервіс
Ось як виглядає стандартний обмін даними під час аутентифікації:
POST /api/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "hashed_and_verified_serverside"
}
Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Сервер перевіряє надані облікові дані, і якщо вони підтверджуються, повертає токен. Відтоді цей токен виступає доказом ідентичності користувача для кожного запиту:
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Проміжний модуль, відповідальний за перевірку цього токена, зазвичай виглядає приблизно так:
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return res.status(401).json({ error: 'Invalid or expired token' });
req.user = decoded;
next();
});
}
Зверніть увагу на те, що цей мідлвер з наміром пропускає. Він жодним чином не вирішує, що саме дозволено користувачеві робити. Він просто підтверджує, що токен є легітимним, та додає розшифрований вміст до об’єкта запиту. Більше нічого не входить до обов’язків механізму автентифікації.
Авторизація: перевірка дозволів
Авторизація починає діяти лише після того, як автентифікація вже вдалась. Вона відповідає на зовсім інше питання: чи дозволено цій перевіреній особі виконати цю конкретну дію?
Деякі поширені підходи до структурування авторизації:
- Контроль доступу на основі ролей (RBAC) — дозволи прив’язуються до ролей, таких як
admin,editorчиviewer
read:messages або write:filesТипова перевірка авторизації виконується після того, як запит вже пройшов процедуру аутентифікації:
function authorize(requiredRole) {
return (req, res, next) => {
if (req.user.role !== requiredRole) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
res.json({ data: 'sensitive admin data' });
});
Це дві окремі функції проміжного програмування, які обробляють різні завдання та виконуються у фіксованому порядку: authenticate визначає, хто є звернувшимся користувачем, а authorize вирішує, чи дозволено цьому користувачеві продовжити. Запит може пройти перевірку та все одно бути відхиленим під час другої перевірки — причина відхилення тут не пов’язана з ідентичністю користувача.
Коди статусу, які фактично відображають цю різницю
Саме тут теоретична різниця перетворюється на щось, що можна буквально перевірити в коді.
401 Unauthorized — назва є оманливою, але цей код насправді вказує на проблему з автентифікацією. Це означає, що сервер не отримав жодних облікових даних, або ті, що він отримав (включаючи токен), є недійсними або закінчили термін дії. На цьому етапі сервер не має жодної підтвердженої інформації про те, хто надсилає запит.
403 Forbidden — цей статус вказує на проблему з авторизацією. Сервер вже визначив, хто є запитувачем; він просто не дозволяє цій особі виконати запитану дію чи отримати потрібний ресурс.
// Authentication failure — identity unknown or invalid
res.status(401).json({ error: 'Invalid credentials' });
// Authorization failure — identity known, permission denied
res.status(403).json({ error: 'You do not have access to this resource' });
Надсилання статусу 401 у випадках, коли насправді йдеться про проблему з правами, є поширеною помилкою. Це змушує клієнта знову увійти, хоча справжня проблема полягає у тому, що процес входу працює без проблем — у обліковому записі просто відсутня необхідна роль. Саме ця плутанина може перетворити те, що мало б бути швидким рішенням проблеми з правами, на тривалу та марну роботу з діагностикою на стороні клієнта у пошуках бага з входом, якого насправді немає.
Де це трапляється у реальних архітектурах
Полезні навантаження JWT часто поєднують ці аспекти без наміру когось. Токен призначений для зберігання даних про ідентичність — таких як sub, email, iat та exp. Чи повинен він також містити дані про дозволи, наприклад role чи scope, — це рішення з боку проектувальника, яке залежить від системи. Включення ролей безпосередньо до токена зручне та дозволяє уникнути додаткових пошуків, але це має серйозну ціну: будь-які зміни у дозволах користувача не набудуть чинності, доки той токен не буде перевиданий. Це справжній компроміс, а не швидкий спосіб без наслідків.
{
"sub": "user_12345",
"email": "user@example.com",
"role": "editor",
"iat": 1710000000,
"exp": 1710003600
}
Системи, побудовані навколо сеансів з боку сервера, зазвичай тримають ці дві функції окремо. Ідентифікатор сеансу відповідає за автентифікацію, тоді як дозволи завантажуються безпосередньо з бази даних при кожному запиті. Це усуває проблему застарілих даних, описану вище, але при цьому потрібен додатковий запит до бази даних за кожен запит.
У архітектурах з мікросервісами автентифікація зазвичай централізована — її обробляє шлюз або постачальник ідентичності, такий як Auth0, Keycloak чи AWS Cognito, який видає токен. Однак авторизація зазвичай залишається на розсуд кожного окремого сервісу, який читає дані з цього токена та застосовує власні правила. В результаті одна й та сама подія автентифікації може бути інтерпретована по-різному десятьма окремими сервісами, кожен з яких самостійно вирішує, що ця ідентичність може робити.
Правило, яке запобігає більшості проблем з безпекою
Аутентифікація має виконуватися перед авторизацією, і ці два етапи ніколи не повинні об’єднуватися в одну спільну перевірку.
// Correct: sequential, separate concerns
app.post('/api/posts/:id/delete', authenticate, authorize('editor'), deleteHandler);
// Wrong: conflating identity with permission in one step
app.post('/api/posts/:id/delete', (req, res) => {
if (req.headers.authorization === 'valid-token-and-also-admin') {
// fragile, unscalable, and impossible to audit correctly
}
});
Система, яка ретельно перевіряє аутентичність, але ігнорує авторизацію, бездоганно підтверджує ідентичність, а потім дозволяє кожній підтвердженій особі робити все, що вона хоче — тож компрометація одного облікового запису з низькими правами фактично означає повний компрометацій системи. Якщо все навпаки, то система з ретельно налаштованою авторизацією, яка працює над слабкою аутентифікацією, має протилежну проблему: ретельно визначені дозволи охороняють „двері“, крізь які будь-хто може пройти без жодних перешкод.
Обидві шари мають бути надійними, застосовуватися у правильному порядку та розглядатися як окремі аспекти. У цьому й полягає вся різниця — і як тільки це належним чином відобразиться у ваших кодах статусу, ланцюзі проміжного програмування та способі проектування токенів, ціла категорія проблем з безпекою просто зникає.
Пов’язана література
- Впровадження токенів доступу та оновлення разом у Node.js — Дізнайтеся, як поєднувати токени доступу з коротким терміном дії з токенами оновлення, які змінюються, у Node.js для досягнення балансу між безпекою та безперешкодними сеансами користувачів.