Autoryzacja a upoważnianie: gdzie każde z nich znajduje się w twoim kodzie
Autoryzacja potwierdza, kim jesteś, a upoważnienie decyduje o tym, co możesz robić. Gdzie każda z nich znajduje się w middleware, 401 kontra 403, oraz błędy wynikające z ich pomieszania.
Źródła: Authentication vs Authorization: The Distinction Most Developers Get Wrong
Dwa słowa, niemal wymienne przy pobieżnym przeczytaniu, a jednak każde z nich reguluje zupełnie inny aspekt logiki systemu.
Jeśli kiedykolwiek odesłałeś kod 401 w sytuacji, która wymagała faktycznie kodu 403, lub uruchomiłeś proces logowania i ogłosiłeś zakończenie autoryzacji, nie zajmując się w ogóle logiką uprawnień, to już napotkałeś problemy wynikające z pomieszania tych dwóch koncepcji. Rozwiążmy je raz na zawsze.
Autoryzacja: Weryfikacja tożsamości
Autoryzacja to etap, w którym system potwierdza, że użytkownik rzeczywiście jest tym, za kogo się podaje. Odpowiada na jedno pytanie: Czy ta tożsamość jest prawdziwa?
Jest to wydarzenie jednorazowe, zwykle występujące podczas logowania, i skutkuje powstaniem jakiegoś rodzaju uprawnień — sesji, tokena lub ciasteczka — które pozwalają reszcie aplikacji ufać żądającemu, bez konieczności ponownej weryfikacji hasła przy każdym żądaniu.
Typowe sposoby implementacji autoryzacji to:
- Autoryzacja oparta na hasłach — podane dane są porównywane z wartością hasła spieczętowaną i przechowywaną w bazie danych
- Autoryzacja oparta na tokenach (JWT) — podpisany token generowany po udanym logowaniu, który jest następnie sprawdzany przy każdym kolejnym żądaniu
- OAuth 2.0 / OpenID Connect — weryfikacja tożsamości jest przekazywana zewnętrznemu dostawcy, takiemu jak Google lub GitHub
- Klucze API — stała, tajna strona tekstowa identyfikująca określonego klienta lub usługę
Oto jak wygląda standardowa wymiana danych przy autoryzacji:
POST /api/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "hashed_and_verified_serverside"
}
Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Serwer weryfikuje podane dane logowania i po ich potwierdzeniu zwraca token. Od tego momentu ten token służy jako dowód tożsamości użytkownika przy każdej prośbie:
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Middleware odpowiedzialny za weryfikację tego tokena zazwyczaj wygląda mniej więcej tak:
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();
});
}
Zwróć uwagę na to, co ten middleware celowo pomija. W żadnym momencie nie decyduje o tym, co użytkownik ma prawo robić. Po prostu potwierdza, że token jest ważny, i dołącza zdekodowany payload do obiektu żądania. Nic więcej nie należy do zakresu zadań autoryzacji.
Uprawnienia: Weryfikacja dostępu
Uprawnienia wchodzą w grę dopiero po pomyślnej autoryzacji. Odpowiadają na zupełnie inne pytanie: Czy ta zweryfikowana tożsamość ma prawo wykonać tę konkretną czynność?
Niektóre z powszechnych podejść do strukturyzowania uprawnień:
- Kontrola dostępu oparta na rolach (RBAC) — uprawnienia są przypisywane rolom takim jak
admin,editorlubviewer
read:messages lub write:filesZwykła weryfikacja uprawnień odbywa się po tym, jak żądanie przeszło już autoryzację:
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' });
});
Są to dwie odrębne funkcje middleware obsługujące dwa różne zadania, wykonywane w ustalonej kolejności: authenticate określa, kim jest osoba wysyłająca żądanie, natomiast authorize decyduje, czy ta osoba ma prawo do dalszego działania. Żądanie może przejść pierwszą weryfikację, a mimo to zostać odrzucone przez drugą – przy czym takie odrzucenie nie ma nic wspólnego z tożsamością.
Kody stanu, które faktycznie odzwierciedlają tę różnicę
Tutaj teoretyczna różnica przeradza się w coś, co można dosłownie sprawdzić w kodzie.
401 Unauthorized – nazwa jest myląca, ale ten kod faktycznie wskazuje na problem z autoryzacją. Oznacza to, że serwer nie otrzymał żadnych uprawnień lub te, które otrzymał (włączając token), są nieważne lub wygasłe. W tym momencie serwer nie ma żadnych pewnych informacji na temat osoby wysyłającej żądanie.
403 Forbidden — ten kod wskazuje na problem z uprawnieniami. Serwer już ustalił, kim jest żądający; po prostu nie pozwala tej tożsamości wykonać żądanej czynności lub uzyskać dostęp do żądanego zasobu.
// 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' });
Wysyłanie kodu 401 w sytuacji, gdy rzeczywiście chodzi o problem z uprawnieniami, to błąd, który występuje ciągle. Polecane jest wtedy ponowne zalogowanie się klienta, mimo że prawdziwym problemem jest to, iż proces logowania przebiega bez zarzutu — konto po prostu nie posiada wymaganej roli. Sam ten błąd może przekształcić to, co powinno być szybkim rozwiązaniem problemu z uprawnieniami, w długą i bezskuteczną sesję debugowania po stronie klienta, polegającą na szukaniu błędu w logowaniu, którego w rzeczywistości nie ma.
Gdzie to występuje w rzeczywistej architekturze
Więźze JWT często łączą te elementy bez żadnego zamiaru ze strony twórców. Token ma służyć do przechowywania danych identyfikacyjnych — takich jak sub, email, iat i exp. To, czy powinien on również zawierać dane uprawnień, takie jak role lub scope, to decyzja projektowa zależna od konkretnego systemu. Umieszczanie informacji o rolach bezpośrednio w tokenie jest wygodne i pozwala uniknąć dodatkowych zapytań, ale wiąże się z poważnym kosztem: wszelkie zmiany w uprawnieniach użytkownika wejdą w życie dopiero po ponownym wydaniu tego tokena. To prawdziwa kompromisowa decyzja, a nie skrót bez żadnych negatywnych konsekwencji.
{
"sub": "user_12345",
"email": "user@example.com",
"role": "editor",
"iat": 1710000000,
"exp": 1710003600
}
Systemy oparte na sesjach po stronie serwera mają tendencję do oddzielania tych dwóch aspektów. Identyfikator sesji służy do autoryzacji, natomiast uprawnienia są pobierane na bieżąco z bazy danych przy każdej prośbie. Dzięki temu unika się problemu przestarzałych danych opisanego powyżej, ale kosztem dodatkowej wywołania bazy danych przy każdej prośbie.
W architekturach mikrosług autoryzacja jest zazwyczaj scentralizowana – obsługiwana raz przez bramkę lub dostawcę tożsamości takiego jak Auth0, Keycloak czy AWS Cognito, który wydaje token. Z kolei autoryzacja jest zwykle pozostawiona poszczególnym usługom, które odczytują informacje zawarte w tym tokenie i stosują własne zasady. W rezultacie jeden event autoryzacyjny może być interpretowany inaczej przez dziesięć oddzielnych usług, z których każda samodzielnie decyduje, co dane identyfikator może robić.
Zasada zapobiegająca większości błędów bezpieczeństwa
Autoryzacja musi zostać przeprowadzona przed upoważnieniem, a te dwa procesy nigdy nie powinny być łączone w jedną wspólną kontrolę.
// 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
}
});
System, który rygorystycznie weryfikuje tożsamość, ale pomija etap upoważniania, doskonale potwierdza tożsamość użytkownika, a następnie pozwala każdej potwierdzonej tożsamości robić to, co chce – więc kompromitacja jednego konta o niskich uprawnieniach oznacza w praktyce całkowity kompromitacja systemu. Z drugiej strony, system z dobrze dostrojonym upoważnieniem, działający nad słabą autoryzacją, ma odwrotny problem: starannie określone uprawnienia chronią drzwi, przez które każdy może swobodnie wejść bez żadnych ograniczeń.
Oba warstwy muszą być solidne, stosowane we właściwej kolejności i traktowane jako odrębne kwestie. To właśnie stanowi całą różnicę — a gdy zostanie to prawidłowo odzwierciedlone w kodach stanu, łańcuchu middleware oraz sposobie projektowania tokenów, cała klasa błędów bezpieczeństwa po prostu znika.