Strona główna / Artykuły / Strategia tokenów odświeżania dla systemów autoryzacji w Node.js

Strategia tokenów odświeżania dla systemów autoryzacji w Node.js

Naucz się, jak projektować, obracać, cofać oraz bezpiecznie przechowywać tokeny odświeżania w Node.js, aby kradzieże tokenów i logowanie wylogowywane działały zgodnie z oczekiwaniami.

2681 słów

Gdy w projekcie Node.js po raz pierwszy uruchomiony zostanie proces logowania, może się wydawać, że praca jest w zasadzie szybko zakończona.

Wzorzec wydaje się dość prosty: użytkownik podaje adres e-mail i hasło, serwer sprawdza te dane w bazie danych, a jeśli się zgadzają, generowany jest token JWT, który następnie jest wysyłany z powrotem do klienta. Ten token towarzyszy potem każdej kolejnej żądaniu. Na pierwszy rzut oka wygląda to jak kompletny system autoryzacji.

Jednak przy bliższym przyjrzeniu się okazuje się, że istnieje długa lista pytań, na które ten prosty proces nigdy nie odpowiada:

  • Co się dzieje, gdy token wygaśnie?
  • Czy od użytkownika oczekuje się, że za każdym razem będzie musiał zalogować się od nowa?
  • Jeśli token zostanie skradziony, jak długo atakujący może go używać?
  • Jak w praktyce wygląda odlogowanie użytkownika?
  • Czy istnieje sposób na cofnięcie tokena, który już został wydany?
  • Co się dzieje, jeśli skradziony token zostanie ponownie wykorzystany, nawet po tym, jak rzekomo został unieszkodliwiony?
  • Gdzie w ogóle powinien być przechowywany taki token na stronie klienta?
  • Żadne z tych pytań nie zostaje odpowiedziane przez stwierdzenie „podpisz JWT i zweryfikuj je później”. Zazwyczaj w tym momencie trzeba przestać polegać na gotowych instrukcjach logowania i rzeczywiście zgłębić, jak działają tokeny odświeżania.

    Dlaczego tokeny dostępu nie trwają długo

    Krótkotrwały token dostępu to nie wartość domyślna, o której ktoś zapomniał zmienić – to świadoma decyzja projektowa.

    Pomyśl o sytuacji, gdy token dostępu pozostaje ważny przez 30 dni i trafi w niepowinne ręce: atakujący ma teraz dostęp do konta tego użytkownika przez cały miesiąc. To nie do przyjęcia kompromis. Dlatego tokeny dostępu są zazwyczaj ograniczane czasowo do kilku minut, a nie tygodni – krótki okres ważności zmniejsza zakres szkód w przypadku wycieku tokena.

    Jednak taka decyzja projektowa stwarza własny problem. Jeśli token wygasa co 15 minut, czy użytkownik musi wpisywać swoje hasło ponownie co 15 minut? Oczywiście to nie jest wykonalne. Tokeny odświeżające istnieją właśnie po to, aby załatać tę lukę.

    A co tak naprawdę robi token odświeżający?

    Na pierwszy rzut oka token odświeżający wygląda jak zwykły token, ale jego rola w systemie jest zupełnie inna niż rola tokena dostępu.

    Proces zazwyczaj przebiega w następujący sposób:

    1. Użytkownik loguje się.
    2. Serwer zwraca zarówno token dostępu, jak i token odświeżający.
    3. Token dostępu jest używany do wysyłania żądań do API.
    4. Ostatecznie token dostępu wygasa.
    5. Klient wysyła token odświeżający do dedykowanego endpointu odświeżania.
    6. Jeśli serwer go pomyślnie zweryfikuje, wydaje zupełnie nowy token dostępu.

    Koncepcją, którą warto sobie utrwalić, jest to, że token odświeżania nigdy nie powinien bezpośrednio wywoływać punktów końcowych API. Jego jedynym zadaniem jest uzyskanie nowego tokena dostępu. Gdy te dwie funkcje zostaną oddzielone w umyśle, reszta projektu zaczyna się układać we właściwe miejsce.

    Token dostępu kontra token odświeżania – porównanie

    Token dostępu służy do korzystania z chronionych API, natomiast token odświeżania jest używany wyłącznie do uzyskania nowego tokena dostępu. Token dostępu ma krótki czas trwania, podczas gdy token odświeżania – dłuższy. Token dostępu jest wysyłany niemal przy każdej prośbie, natomiast token odświeżania jest używany tylko sporadycznie. Wyciek tokena dostępu jest problematyczny, ale wyciek tokena odświeżania jest jeszcze gorszy, ponieważ może być wykorzystany do ciągłego tworzenia nowych tokenów dostępu. Token dostępu jest sprawdzany przy każdym wywołaniu API, natomiast token odświeżania jest sprawdzany tylko przez dedykowaną logikę odświeżania lub sesji.

    Często przytaczanym podziałem jest około 15 minut trwania tokena dostępu i 7 dni dla tokena odnowienia, ale te liczby nie mają nic magicznego. Odpowiednie wartości zależą wyłącznie od tego, co mogą zaakceptować twoja aplikacja oraz jej użytkownicy.

    Dlaczego tokeny odnowienia zasługują na większy szacunek, niż sugeruje pierwszy rzut oka

    Token odnowienia może skutecznie utrzymywać sesję aktywną tak długo, jak długo pozostaje ważny. Jeśli atakujący go zdobędzie, nie ma do dyspozycji tylko jednego okna dostępu – może nadal generować nowe tokeny dostępu, dopóki ten token odnowienia nie wygaśnie lub nie zostanie anulowany.

    Ta rzeczywistość zmienia sposób, w jaki należy traktować ten token. To nie jest zwykły element danych aplikacji; zachowuje się bardziej jak fizyczny klucz do domu. Takie traktowanie oznacza konieczność zajęcia się następującymi kwestiami:

    • wygaśnięcie
    • anulowanie
    • rotacja
  • gdzie i w jaki sposób jest przechowywany
  • w jaki sposób jest transportowany
  • wykrywanie ponownego użycia
  • co tak naprawdę powinno robić logowanie wylogowania
  • zarządzanie sesjami jako całość
  • Zazwyczaj w tym momencie pozornie prosta konfiguracja JWT zaczyna ujawniać swoje słabe punkty.

    Rotacja: uniemożliwienie wiecznego istnienia jednego tokena

    Koncepcją, która zmienia sposób projektowania tego systemu, jest rotacja tokenów odświeżania. Zamiast pozwalać na nieskończone ponowne użycie jednego tokena odświeżania, serwer wydaje nowy za każdym razem, gdy bieżący zostanie pomyślnie użyty, a stary jest natychmiast usuwany.

    Zamiast jednego długowiecznego tokena krążącego wiecznie, mamy do czynienia z łańcuchem tokenów, w którym każdy zastępuje poprzedni:

    Wykorzystywany jest Token A, co powoduje wydanie Tokena B w momencie, gdy A zostaje wycofany. Następnie wykorzystywany jest Token B, co skutkuje wydaniem Tokena C, gdy B zostaje wycofany. Ten wzorzec trwa nieprzerwanie, przy czym jedynie najnowszy token w łańcuchu pozostaje ważny.

    Dlaczego to faktycznie pomaga

    Rozważmy sytuację, w której atakujący jakoś uzyskuje kopię Tokena A, podczas gdy prawowity użytkownik nadal go posiada.

    Jeśli prawdziwy użytkownik przypadkowo użyje go pierwszy, Token A zostaje oznaczony jako wykorzystany i faktycznie unieważniony, a w jego miejsce wydawany jest Token B. Gdy atakujący później spróbuje użyć tego samego Tokena A, serwer rozpoznaje go jako token już wykorzystany, co stanowi oczywisty sygnał ostrzegawczy. W zależności od tego, jak surowo skonfigurowany jest system, to wykrycie może spowodować odwołanie całego łańcucha tokenów związanych z tą sesją, a nie tylko jednego kompromitowanego.

    To stawia cię w znacznie silniejszej pozycji niż w sytuacji, gdy ten, kto pierwszy przejmie token, faktycznie zyskuje kontrolę na zawsze.

    Anulowanie, ponieważ wylogowanie powinno naprawdę coś oznaczać

    JWT-y są często opisywane jako bezstanowe, a z technicznego punktu widzenia jest to prawdą. Jednak każdy system w praktyce wymaga przynajmniej pewnego stanu, a wylogowanie to najwyraźniejszy przykład tego potrzeby.

    Jeśli użytkownik kliknie „wyloguj się”, a jedyne, co się stanie, to usunięcie tokena z interfejsu użytkownika, sam token pozostaje w pełni ważny aż do naturalnego wygaśnięcia. Serwer nie ma pojęcia, że użytkownik „wylogował się” w jakimkolwiek znaczącym sensie, więc będzie nadal akceptować ten sam token, jeśli zostanie ponownie przedstawiony przed jego wygaśnięciem.

    Tokeny odnowy stanowią rzeczywisty mechanizm do śledzenia i zamykania sesji po stronie serwera. Przed wylogowaniem token sesji znajduje się w stanie aktywnym. Gdy nastąpi wylogowanie, ten token powinien zostać uznany za anulowany, a wszelkie późniejsze próby jego użycia do odnowy powinny zakończyć się niepowodzeniem. Taki właśnie wygląda prawdziwe wylogowanie, a nie to czysto pozorne.

    Wylogowanie powinno odbywać się po stronie backendu, a nie tylko frontendu

    Społeczna wersja wylogowania, którą często implementują początkujący, polega jedynie na usunięciu tokena z lokalnego przechowywania lub z dowolnego innego miejsca, gdzie klient go przechowuje, a następnie kontynuowaniu pracy.

    Bardziej kompletny i rzetelny proces wylogowania zazwyczaj przebiega według następującej sekwencji:

    1. Klient wysyła żądanie POST /auth/logout
    2. Serwer określa, do której sesji odnosi się to żądanie
  • Serwer cofa token odświeżania lub sesję z nim powiązaną
  • Dopiero wtedy klient usuwa swój własny lokalny stan
  • Kluczowym punktem, który warto tu podkreślić, jest to, że wylogowanie to zasadniczo zdarzenie bezpieczeństwa po stronie serwera. Jeśli Twoja implementacja wylogowania dotyczy tylko danych przechowywanych na stronie klienta, w rzeczywistości nie wylogowałeś użytkownika – po prostu sprawiłeś, że interfejs zapomniał o jego istnieniu.

    Decydowanie o miejscu przechowywania tego tokena

    Wybór miejsca przechowywania ma większe znaczenie, niż się na pierwszy rzut oka wydaje. W aplikacjach opartych na przeglądarce powszechnym podejściem jest umieszczenie tokena odświeżania w ciasteczku skonfigurowanym z kilkoma specyficznymi atrybutami:

    • HttpOnly, który uniemożliwia bezpośrednie odczytanie go przez JavaScript po stronie klienta
    • Secure, który ogranicza jego przesyłanie wyłącznie przez HTTPS
  • SameSite, który zmniejsza narażenie na określone kategorie ataków międzystronniczych
  • Jednak żadna z tych ustawień nie czyni pliku cookie automatycznie odpornym na ataki. Nadal musisz wziąć pod uwagę zabezpieczenia CSRF, sposób określania zakresu domeny i ścieżki, procedurę wygaśnięcia sesji oraz to, w jaki sposób proces wylogowania oddziałuje we współpracy ze wszystkimi tymi elementami. Nie istnieje żaden uniwersalny wzorzec przechowywania, który można by skopiować z tutorialu i zaufać mu bez dostosowania go do architektury własnego systemu.

    Co się stanie, jeśli token odświeżania i tak wycieknie?

    To właśnie ten konkretny scenariusz sprawił, że znaczenie rotacji tokenów stało się oczywiste.

    Jeśli zarówno uprawniony użytkownik, jak i atakujący w jakiś sposób dostaną do ręki ten sam token odnowienia, a wasz system pozwala na nieograniczone ponowne użycie tego tokena, to naprawdę nie ma sposobu na odróżnienie tych dwóch stron. Z perspektywy serwera obie prośby wyglądają tak samo prawomocnie.

    Dzięki zastosowaniu rotacji pierwsze użycie tego tokena powoduje jego natychmiastowe wycofanie. Dlatego jeśli ten sam token zostanie przedstawiony po raz drugi, jest to zachowanie nieprawidłowe i stanowi sygnał. Dobrze zaprojektowany system może potraktować to ponowne użycie jako ostrzeżenie i zareagować odpowiednio – czy to poprzez anulowanie sesji, oznaczenie konta lub zastosowanie polityki odpowiadającej waszemu poziomowi ryzyka.

    To właśnie jest główny powód, dla którego tokeny odnowienia powinny być traktowane jako wyzwanie w zakresie projektowania bezpieczeństwa, a nie coś, co można rozwiązać po prostu poprzez utworzenie kolejnego JWT.

    Tokeny odświeżania również muszą wygasnąć

    Latwo o to zapomnieć, ale tokeny odświeżania również nie powinny być trwałe. Bez terminu wygaśnięcia skradziony token odświeżania staje się w praktyce bramą tylową, która nigdy się nie zamyka.

    Powszechnym punktem wyjścia jest około 15 minut dla tokenów dostępu w połączeniu z 7 dniami dla tokenów odświeżania, chociaż dokładne liczby, które wybierzesz, powinny odzwierciedlać twoją tolerancję na ryzyko, a nie wartości podane w pierwszym tutorialu, jaki natrafisz.

    Warto wyraźnie odróżniać w umyśle czas trwania tokena od czasu trwania sesji, ponieważ to nie są to samo pojęcie. Sesja może pozostawać aktywna przez długi czas dzięki powtarzanym cyklom rotacji, mimo że każdy pojedynczy token istnieje tylko przez krótki okres.

    Błędy, których warto unikać lub na które zwracać uwagę

    • Tokeny dostępu, które istnieją zbyt długo, co zwiększa ryzyko w przypadku ich ujawnienia
    • Tokeny odświeżania bez żadnego terminu ważności, co umożliwia trwałe dostanie się do systemu
    • Pomijanie całkowicie rotacji tokenów, co utrudnia wykrycie kradzieży
    • Niedbalstwo w obchodzeniu się z poufnymi danymi, co powoduje umieszczanie tokenów w dziennikach, adresach URL lub pamięci po stronie klienta, gdzie nie powinny się znajdować

    Rzeczywiste testowanie procesu odświeżania

    Proces autoryzacji wymaga takiego samego stopnia testowania jak każda inna krytyczna ścieżka w aplikacji. Oto podstawowa sekwencja, którą warto przetestować:

    1. Zaloguj się i upewnij się, że otrzymujesz zarówno token dostępu, jak i token odnowienia
    2. Wezwij chronioną trasę za pomocą ważnego tokena dostępu i upewnij się, że otrzymujesz odpowiedź 200 OK
    3. Wezwij chronioną trasę za pomocą wygasłego tokena dostępu i upewnij się, że otrzymujesz kod 401
    4. Wezwij punkt końcowy odnowienia i upewnij się, że otrzymujesz nowy token dostępu (oraz nowy token odnowienia, jeśli jest włączone rotowanie)
    5. Spróbuj ponownego użycia starego tokena odnowienia, który został już zastąpiony, i upewnij się, że zostanie odrzucony
    6. Wyloguj się, a następnie spróbuj odnowić sesję, która jest już nieskuteczna, i upewnij się, że również zostanie odrzucona

    Rozwiązywanie problemów na tym etapie jest znacznie tańsze niż ich odkrywanie po uruchomieniu aplikacji.

    Pełny obraz sytuacji

    Login
      ↓
    Access Token + Refresh Token issued
      ↓
    API requests using Access Token
      ↓
    Access Token expires
      ↓
    Refresh Token sent to refresh endpoint
      ↓
    Server validates session
      ↓
    Refresh Token rotated
      ↓
    New Access Token issued
      ↓
    API requests continue
    

    Jeśli walidacja zawiedzie w dowolnym momencie tej sekwencji odświeżania — token wygasł, został cofnięty lub po prostu jest nieważny — odpowiedzią będzie kod 401, a użytkownik musi ponownie zalogować się od początku. To rozdzielenie „krótkotrwałego uprawnienia do wywoływania API” od „dłużej trwającego uprawnienia do pozostawania zalogowanym” stanowi właśnie istotę całego tego rozwiązania, streścić ją można w jednym zdaniu.

    Listwa kontrolna przed produkcją

    Projektowanie tokenów

    • Czy tokeny dostępu rzeczywiście szybko wygasają?
    • Czy tokeny odświeżania mają własny termin ważności?
    • Czy tokeny odświeżania są dostępne tylko w punkcie końcowym do odświeżania, a nie mogą być używane na dowolnych trasach?
    • Czy treść tokenów unika przenoszenia większej ilości informacji, niż to konieczne?

    Bезpieczeństwo

    • Czy we wszystkich przypadkach wymagany jest HTTPS?
  • Czy tokeny odświeżania są przechowywane w bezpiecznym miejscu?
  • Czy sekrety są trzymane oddzielnie od kodu źródłowego?
  • Czy tokeny są usuwane z logów?
  • Czy ochrona CSRF jest stosowana tam, gdzie jest to konieczne?
  • Zarządzanie sesjami

    • Czy można odwołać token odświeżania na żądanie?
    • Czy wylogowanie następuje na serwerze, a nie tylko po stronie klienta?
    • Czy faktycznie wdrożono rotację tokenów?
    • Czy można wykryć, gdy token zostanie ponownie użyty?

    Zakres testów

    • Ważny token odświeżania powoduje sukces
    • Wygasły token odświeżania powoduje niepowodzenie
    • Odwołany token odświeżania powoduje niepowodzenie
    • Błędnie sformatowany lub nieważny token odświeżania powoduje niepowodzenie
    • Token, który został wcześniej zrotowany, powoduje niepowodzenie
    • Prawidłowe wylogowanie zamyka odpowiednią sesję

    Podsumowanie

    Autoryzacja to nie tylko potwierdzenie tożsamości osoby w momencie logowania. Chodzi o podjęcie decyzji — a następnie jej faktyczne egzekwowanie — o tym, jak długo ta zaufanie powinno obowiązywać później.

    JWT upraszczają część związaną z „sprawdzeniem, kim jesteś”. Tym, czego nie oferują za darmo, są zarządzanie sesją, odwoływanie uprawnień, prawidłowe wylogowanie, ochrona przed skradzionymi tokenami, wykrywanie ponownego użycia, bezpieczne przechowywanie oraz rozsądne okna wygaśnięcia. Każda z tych kwestii to świadoma decyzja, którą musisz sam podjąć. Im większe i bardziej używane staje się twoje aplikacje, tym ważniejsze stają się te decyzje.

    Co dalej

    Gdy tokeny dostępu i odnowienia wreszcie zaczynają mieć sens, kolejną dziedziną wartą zgłębienia jest zarządzanie sesją w większym skali:

    • Jak należy postępować z użytkownikiem, który pozostaje zalogowany na pięciu różnych urządzeniach?
  • Czy użytkownicy powinni mieć możliwość przeglądania — i zamykania — swoich aktywnych sesji?
  • Czy możliwe jest wylogowanie kogoś z jednego urządzenia bez kończenia wszystkich jego sesji?
  • Jak wygląda „podejrzana” sesja i jak ją oznaczyć?
  • Jaki jest właściwy sposób przechowywania rodzin tokenów odświeżania?
  • W którym momencie model sesji oparty na bazie danych staje się bardziej sensowny niż pozostanie w pełni bezstanowym z użyciem JWT?
  • Napisanie samej ścieżki logowania może wymagać dziesięciu linijek kodu. Stworzenie systemu autoryzacji, któremu można naprawdę zaufać, wymaga znacznie więcej wysiłku.

    Literatura pokrewna

  • Dlaczego dekodowanie kawałków bufora jako tekstu niszczy przesyłanie plików — Wyjaśnia, jak traktowanie binarnych danych buforowych jako tekstu UTF-8 potajemnie uszkadza przesyłane pliki, oraz pokazuje właściwe sposoby obsługi na poziomie bajtów, aby temu zapobiec.
  • Seanse vs. JWT: Wybór odpowiedniego modelu autoryzacji w Node.js — Dowiedz się, jak seanse i JWT różnią się w kontekście autoryzacji w Node.js, gdzie każdy z nich zawodzi, oraz jak wybrać między nimi bez późniejszych żalów.
  • Wdrożenie jednocześnie tokenów dostępu i odnowienia w Node.js — Poznaj sposoby łączenia krótkotrwałych tokenów dostępu z rotującymi tokenami odnowienia w Node.js, aby zapewnić równowagę pomiędzy bezpieczeństwem a płynnością sesji użytkowników.