Mapowanie słownika autoryzacji: klucze API, sesje, JWT, OAuth2, OIDC, SSO
Dowiedz się, jak klucze API, sesje, JWT, OAuth2, OpenID Connect oraz SSO współpracują ze sobą, klasyfikując każdy z nich pod jednym pytaniem: kto wywołuje, lub co może robić.
Rozwijający dodaje opcję „Zaloguj się przez Google” do projektu, otrzymuje token dostępu i uważa, że zadanie jest zakończone. Wtedy kolega zadaje proste pytanie: skąd aplikacja wie, o który użytkownik chodzi? Często uczciwa odpowiedź brzmi, że nie wie, ponieważ zaimplementowano wtedy upoważnienie delegowane, a nie logowanie. Pojęcia takie jak JWT, sesja, OAuth2, OIDC i SSO są zazwyczaj wyjaśniane pojedynczo, co właśnie powoduje ich pomieszanie. Ten artykuł umieszcza każde z nich na jednej mapie, dzięki czemu można łatwo określić, jaki problem rozwiązuje dany element Twojej architektury, oraz wybrać odpowiedni narzędzie do kolejnego przypadku.
Dwa pytania stojące za każdym mechanizmem autoryzacji
Prawie wszystko w tej dziedzinie odpowiada na jedno z dwóch pytań.
- Autoryzacja pyta kto jesteś? Jest to proces weryfikacji tożsamości, niezależnie od tego, czy ta tożsamość należy do osoby, usługi backendowej czy urządzenia.
- Prawa dostępu pyta co możesz robić? Po potwierdzeniu tożsamości określa, jakie zasoby i działania są dostępne.
HTTP już koduje tę różnicę w swoich kodach stanu. Odpowiedź 401 Unauthorized oznacza, że osoba wysyłająca żądanie nie udowodniła swojej tożsamości (mimo mylącego nazwy, chodzi tu o autoryzację). Odpowiedź 403 Forbidden oznacza, że serwer dokładnie wie, kto wysyła żądanie, ale mimo to je odrzuca. Jeśli twoja API zwraca 403 z powodu braku tokena lub 401 w przypadku użytkownika bez uprawnień, klienci nie mogą decydować, czy poprosić o logowanie, czy pokazać komunikat „brak dostępu”.
Analogia fizyczna pomaga w zrozumieniu. Przy wejściu do biura ochroniarz sprawdza twoją kartę identyfikacyjną pracownika – to jest autoryzacja. Gdy już wejdziesz do środka, twoja odznaka otwiera niektóre drzwi, a inne nie – to jest upoważnienie. Jedno kroku służy do ustalenia tożsamości, drugi do przyznania uprawnień, a system może pomyślnie przeprowadzić pierwszy krok, ale nie udźwignąć drugiego. Aby lepiej zrozumieć, gdzie każda kontrola powinna znaleźć się w kodzie aplikacji, zapoznaj się z autoryzacją i upoważnieniem oraz tym, gdzie każde z nich powinno się znajdować.
Miej te dwa pytania na myśli podczas analizy reszty struktury. Każdy z poniższych mechanizmów staje się łatwiejszy do zrozumienia, gdy wiesz, na które z pytań odpowiada.
Klucze API: identyfikacja klienta, a nie osoby
Klucze API stanowią najprostszą formę autoryzacji klienta. Dostawca wydaje unikalną ciąg znaków, który wysyłasz z każdym żądaniem, a serwer porównuje go z kluczami przechowywanymi w swoich rejestrach. Wiele API akceptuje je jako uprawnienia nośnikowe w standardowym nagłówku:
Authorization: Bearer your-api-key-here
Inni dostawcy używają własnego nagłówka, np. X-API-Key; zasada jest taka sama. Klucz nie służy do opisywania żadnej osoby – jest to wartość niewidzialna dla użytkownika, więc serwer musi ją sprawdzić, aby dowiedzieć się, który konto dokonuje żądania. Nie ma wbudowanego terminu ważności, chyba że sam go ustalisz, a osoba, która ma wyciekły klucz, ma takie same uprawnienia jak jego prawowity właściciel. Dlatego rotacja kluczy, określanie zakresu ich dostępu oraz bezpieczne przechowywanie są twoją odpowiedzialnością.
Wciąż istnieje również autoryzacja HTTP Basic, która wysyła nazwę użytkownika i hasło zakodowane w formacie Base64 w nagłówku. Base64 to kodowanie, a nie szyfrowanie, więc autoryzacja Basic jest bezpieczna tylko przy użyciu TLS i trudno ją uzasadnić poza bardzo prostymi narzędziami wewnętrznymi. Klucze API to częściej wybierana, lżejsza opcja – sprawdzają się dobrze, gdy kontrolujesz obie strony połączenia lub gdy usługa pobiera opłaty i ogranicza liczbę zapytań na konto.
Zwykłe miejsca, gdzie spotykamy klucze API:
- API modeli AI
- Bramy płatności
- API dotyczące pogody i innych danych
- Zapytania między usługami działającymi w tle
Zwróć uwagę, czego brakuje na tej liście: końcowych użytkowników logujących się do twojej aplikacji. Klucz API identyfikuje aplikację lub konto wykonujące zapytanie, a nie osobę siedzącą przed ekranem.
Sesje: stan po stronie serwera przechowywany w pliku cookie
Zanim JWT stały się popularne, sesje były standardowym sposobem na utrzymywanie użytkowników zalogowanych i nadal stanowią dobrą opcję dla aplikacji renderowanych na serwerze.
Proces jest prosty: użytkownik podaje swoje dane logowania, serwer je weryfikuje i tworzy rekord sesji w jakimś magazynie (pamięci, Redis lub bazie danych), a odpowiedź zawiera plik cookie z samym identyfikatorem sesji. Przy każdej kolejnej prośbie przeglądarka wysyła ten plik cookie z powrotem, a serwer szuka identyfikatora, aby odnaleźć sesję oraz dołączonego do niej użytkownika.
Taki układ jest zorientowany na stan, ale to właśnie stanowi jego mocną stronę. Serwer przechowuje prawdziwe dane, więc wylogowanie użytkownika lub anulowanie kompromitowanej sesji polega jedynie na usunięciu odpowiedniego rekordu. Sam plik cookie nie zawiera żadnych poufnych informacji poza losowym identyfikatorem.
Koszt pojawia się przy skalowaniu w poziomie. Jeśli kilka serwerów obsługuje ruch, wszystkie muszą mieć dostęp do tego samego magazynu sesji; w przeciwnym razie użytkownik zalogowany na jednej instancji będzie wyglądał jak anonimowy na następnej. W praktyce udana implementacja wspólnego instancji Redis dobrze rozwiązuje ten problem, ale to kolejny komponent, który trzeba zainstalować, monitorować i prawidłowo skonfigurować. Błędnie skonfigurowany magazyn sesji to typ problemu, który pojawia się w najgorszym możliwym momencie podczas wypuszczania nowej wersji.
Sesje nadal są doskonałym rozwiązaniem dla:
- Klasycznych aplikacji internetowych renderowanych na serwerze
- Paneli administracyjnych
- Aplikacji, w których natychmiastowe cofnięcie uprawnień jest ważniejsze niż brak stanu
JWT: samodzielne, podpisane i czytelne
Token JSON Web to podpisany obiekt JSON zawierający informacje takie jak identyfikator użytkownika, role oraz czas wygaśnięcia. Składa się on z trzech segmentów zakodowanych w formacie base64url połączonych kropkami:
Header.Payload.Signature
Nazwa nagłówka wskazuje algorytm podpisywania, treść wiadomości zawiera dane identyfikacyjne, a podpis umożliwia serwerowi potwierdzenie, że żadna z tych części nie została zmieniona przez kogokolwiek bez klucza podpisywania. Ponieważ dane identyfikacyjne znajdują się wewnątrz tokena, serwer może zweryfikować żądanie poprzez sprawdzenie podpisu i odczytanie treści wiadomości, bez konieczności wyszukiwania w bazie danych. To właśnie ta cecha sprawia, że JWT nadają się do systemów bezstanowych i rozproszonych.
Podpis nie oznacza tajemnicy
Najczęstszym nieporozumieniem jest to, że JWT ukrywa swoją zawartość. Tak nie jest. Standardowe JWT jest podpisane, a nie szyfrowane, więc każdy, kto je posiada, może odszyfrować jego zawartość za pomocą pojedynczej operacji base64. Nigdy nie umieszczaj haseł, danych osobowych, których nie chciałbyś pokazać użytkownikowi, ani wewnętrznych tajemnic w polach tego tokena pod przyzwoleniem, że są one poufne. Gdy występuje incydent związany z JWT, często u jego podstaw leży właśnie to błędne założenie. (Tokeny szyfrowane istnieją w ramach specyfikacji JWE, ale są to odrębny format i rzadko to, co w przewodnikach rozumie się przez „JWT”.)
Tokeny dostępu i odnowienia
Ponieważ JWT pozostaje ważne aż do wygaśnięcia, standardowy model łączy dwa tokeny:
- Token dostępu: krótkotrwały, zazwyczaj od 15 minut do godziny, i wysyłany przy każdej prośbie do API.
Zachowaj token odświeżania w pliku cookie typu HttpOnly, a nie w localStorage, gdzie każdy wstrzyknięty skrypt mógłby go odczytać. Krótki czas trwania toku dostępu ogranicza skutki wycieku, natomiast to właśnie token odświeżania może zawierać logikę cofania uprawnień.
JWT doskonale sprawdzają się w API, aplikacjach mobilnych, aplikacjach jednostronicowych oraz usługach rozproszonych. Nie sprawiły one, że sesje stały się przestarzałe; zastępują łatwe cofanie uprawnień brakiem stanu, a właściwy wybór zależy od Twojej architektury. Porównanie sesji i JWT jako modeli autoryzacji w Node.js szczegółowo omawia ten kompromis.
OAuth 2.0: uprawnienia delegowane, nie logowanie
OAuth 2.0 to koncepcja, która najczęściej trafia do niewłaściwej kategorii. Jest to rama autoryzacji, a nie protokół uwierzytelniania. Jej zadaniem jest umożliwienie jednej aplikacji dostępu do zasobów przechowywanych przez inny serwis w imieniu użytkownika, bez konieczności przekazywania przez niego swojego hasła.
W typowym procesie kodu autoryzacji Twoja aplikacja kieruje użytkownika na ekran zgody dostawcy, na którym pojawia się pytanie w stylu „Czy chcesz pozwolić tej aplikacji na przeglądanie plików w Google Drive?”. Jeśli użytkownik się zgodzi, dostawca odsyła go z krótkotrwałym kodem autoryzacji. Twoje serwerowe oprogramowanie wymienia ten kod na token dostępu, a następnie używa go do wywołania API dostawcy.
Tok dostępu reprezentuje uprawnienie do uzyskiwania dostępu do określonych zasobów. Nie jest to informacja o tym, kim jest użytkownik, a specyfikacja OAuth2 nie określa jego formatu ani nie wymaga, aby aplikacja potrafiła go odczytać. Traktowanie przybycia toku dostępu jako dowodu na to, że użytkownik jest zalogowany, to klasyczny błąd opisany we wstępnym scenariuszu, który doprowadził do rzeczywistych luk bezpieczeństwa – na przykład gdy token wydany jednej aplikacji jest akceptowany jako dowód tożsamości przez inną.
OAuth2 został stworzony, aby odpowiadać na pytania takie jak:
- Czy ta aplikacja może odczytywać pliki użytkownika w Drive?
- Czy ta aplikacja może uzyskać dostęp do repozytoriów użytkownika?
Nie został stworzony, aby odpowiadać na pytanie: kim jest ten użytkownik?
OpenID Connect: warstwa tożsamości nad OAuth2
Jeśli OAuth2 informuje o tym, do czego może mieć dostęp aplikacja, to OpenID Connect dodaje brakującą informację o tym, kim jest dana osoba. OIDC to cienka warstwa identyfikacyjna zbudowana na bazie OAuth2, która wykorzystuje jego mechanizmy przekierowań i wymiany tokenów. Gdy klikasz „Zaloguj się przez Google”, proces działający w tle to OIDC, a nie zwykły OAuth2.
Po uwierzytelnieniu użytkownika dostawca OIDC zwraca dwa tokeny o różnych przeznaczeniach:
- Token ID, który jest JWT opisującym użytkownika: stabilnym, unikalnym identyfikatorem, zawierającym zazwyczaj takie dane jak imię i adres e-mail.
- Token dostępu, który umożliwia aplikacji wykonywanie żądań do API w imieniu użytkownika.
Tok ID służy do uwierzytelniania użytkownika w twojej aplikacji. Token dostępu upoważnia do wykonywania kolejnych działań w aplikacji. Twój backend powinien zweryfikować podpis, emitenta, grupę docelową oraz datę wygaśnięcia toku ID przed zaufaniem jego informacjom, a także powinien przypisywać konta użytkowników do stabilnego identyfikatora, a nie do adresu e-mail, który może ulec zmianie. Patrząc w ten sposób, sam OAuth2 nie może zapewnić logowania; OIDC uzupełnia tę funkcję.
SSO: doświadczenie zapewniane przez protokoły
Jednokrotne logowanie jest często mylone z protokołami, które je implementują. SSO to wzorzec doświadczenia użytkownika: uwierzytelnij się raz, a następnie przemieszczaj się między różnymi systemami bez ponownego logowania. Znajomym przykładem jest Google – zaloguj się raz, a Gmail, Drive, Kalendarz i YouTube będą cię rozpoznawać.
Dwa protokoły wykonują większość pracy za plecami SSO:
- SAML: oparty na XML i dojrzały, dominujący w środowiskach korporacyjnych takich jak portale firmowe, tablice kontrolne CRM oraz narzędzia wewnętrzne.
- OpenID Connect: oparty na JSON i JWT, nowszy rozwiązanie, zwykle preferowany w aplikacjach internetowych i mobilnych.
SAML jest znany z trudności w implementacji i debugowaniu, dlatego przy tworzeniu nowego systemu OIDC jest zazwyczaj łatwiejszym wyborem. SAML nadal ma swoje zastosowanie, gdy konieczna jest integracja z dostawcami tożsamości korporacyjnymi, które nie oferują OIDC, a takie rozwiązania są nadal szeroko używane.
Gdy ktoś prosi o „dodanie SSO”, pierwszą rzeczą do ustalenia jest to, jaki protokół używa jego dostawca tożsamości. Ta jedna odpowiedź określa biblioteki, konfigurację oraz prace testowe, które będą następnie wykonywane.
Wybór mechanizmu
Podsumowując, przybliżony przewodnik decyzyjny wygląda następująco:
- Plik usługi jest wywoływany przez inne programy, a nie osoby: klucze API, z ograniczonym zakresem i rotujące się, lub dane klienta OAuth2 w przypadku potrzeby tokenów wygasających.
- Aplikacja renderowana na serwerze lub panel administracyjny z własnym logowaniem: sesje przy użyciu bezpiecznego ciasteczka
HttpOnly. - API bez stanu, klienci mobilni lub wiele usług weryfikujących tego samego użytkownika: tokeny dostępu JWT z tokenami odnowienia.
- Aplikacja musi działać z danymi użytkownika w innej usłudze: OAuth 2.0.
- Chcesz, aby użytkownicy logowali się przy użyciu istniejącego konta, np. Google: OpenID Connect.
- Pracownicy potrzebują jednego logowania we wielu narzędziach wewnętrznych lub SaaS: SSO przez OIDC lub SAML, jeśli wymaga tego dostawca tożsamości.
Te opcje mogą być łączone. Typowy produkt może używać OIDC do logowania, a następnie wydawać własne sesje lub tokeny JWT, jednocześnie przechowując tokeny dostępu OAuth2 dla integracji z podmiotami trzecimi.
Częste pytania
Jak różnią się w praktyce autoryzacja i uwierzytelnianie? Autoryzacja sprawdza uprawnienia i kończy się błędem HTTP 403, natomiast uwierzytelnianie weryfikuje tożsamość i kończy się błędem 401. Są to odrębne kroki z różnymi kodami błędów, a ich łączenie prowadzi do poważnych luk w bezpieczeństwie.
Czy należy używać tokenów JWT czy sesji? Sesje sprawdzają się w aplikacjach renderowanych na serwerze, które mogą dzielić się magazynem sesji. Tokeny JWT są bardziej odpowiednie dla API bez stanu, klientów mobilnych oraz systemów rozproszonych. Decyzję należy podjąć na podstawie architektury i potrzeb odwołania tokenów, a nie tego, która opcja brzmi nowocześniej.
Czy OAuth2 służy do autoryzacji czy upoważniania? Do upoważniania. Umożliwia aplikacjom dostęp do zasobów w imieniu użytkownika bez konieczności potwierdzania, kim jest ten użytkownik. Do identyfikacji używaj OpenID Connect, który rozszerza OAuth2 właśnie w tym celu.
Główne wnioski
- Klasyfikuj każdy mechanizm pod jednym pytaniem: kto wywołuje? lub co może robić?
- Klucze API identyfikują aplikacje klienta; nie zawierają informacji o tożsamości użytkownika i wymagają określonego terminu ważności oraz procedury rotacji.
- Sesje przechowują stan na serwerze, co ułatwia jego cofnięcie, ale wymaga wspólnego magazynu na większą skalę.
- JWT są podpisane, a nie szyfrowane; trzymaj sekrety poza treścią wiadomości oraz tokeny odnowienia poza
localStorage. - OAuth2 zapewnia upoważnienie delegowane; OIDC dodaje token ID, który faktycznie loguje użytkownika.
Największe zamieszanie w tej kwestii, w tym pomyłka związana z opcją „Zaloguj się przez Google” wspomnianą na początku, wynika z łączenia tych dwóch kwestii. Trzeba trzymać tożsamość i uprawnienia oddzielnie – wtedy wybór między tymi narzędziami staje się kwestią dopasowania każdego z nich do pytania, na które zostało zaprojektowane.