Co naprawdę zawiera token JSON Web Token

Wnętrze tokenu JWT z objaśnieniem nagłówka, ładunku i podpisu, standardowe oświadczenia oraz powód, dla którego zdekodowanie tokenu z Base64url to nie to samo co jego zweryfikowanie.

Token JSON Web Token (JWT) to trzy fragmenty tekstu połączone kropkami. Każdy fragment jest zakodowany w Base64url, a dwa pierwsze to zwykły JSON. Każdy, kto ma token, może odczytać, co jest w środku. Nic w zwykłym tokenie JWT nie jest zaszyfrowane.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9      <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0   <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW     <- signature

Połączone kropkami dają token, który wkleja się do żądania. Zdekoduj dwie pierwsze części, a otrzymasz zwykły JSON:

{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }

Prawdziwy ładunek niesie zwykle również znaczniki czasu i odbiorcę:

{
  "iss": "https://auth.example.com",
  "sub": "12345",
  "aud": "api.example.com",
  "iat": 1756000000,
  "exp": 1756003600,
  "roles": ["editor"]
}

Nagłówek

Nagłówek opisuje sposób podpisania tokenu. Największe znaczenie mają dwa pola:

  • alg nazywa algorytm podpisu, na przykład HS256 (HMAC z SHA-256, z użyciem współdzielonego sekretu) albo RS256 (podpis RSA z SHA-256, składany kluczem prywatnym i sprawdzany publicznym).
  • kid, czyli identyfikator klucza, jest opcjonalny i mówi odbiorcy, którego z kilku kluczy użyto, dzięki czemu klucze można rotować bez unieważniania istniejących tokenów.

typ to zwykle po prostu JWT i nie niesie żadnego znaczenia dla bezpieczeństwa.

Ładunek i jego oświadczenia

Ładunek to obiekt JSON złożony z oświadczeń, co jest po prostu słowem, którym specyfikacja nazywa stwierdzenia o podmiocie tokenu. Część nazw oświadczeń jest zarejestrowana w standardzie i wszystkie są krótkie:

OświadczenieNazwaCo zawiera
issWystawcaKto utworzył token, zwykle adres URL albo nazwa usługi
subPodmiotKogo token dotyczy, zwykle stały identyfikator użytkownika
audOdbiorcaDla kogo token jest przeznaczony, żeby usługa mogła odrzucić tokeny przeznaczone dla innej
expCzas wygaśnięciaChwila, po której token musi zostać odrzucony
nbfNie wcześniej niżChwila, przed którą token musi zostać odrzucony
iatCzas wystawieniaKiedy token został utworzony
jtiIdentyfikator JWTUnikalny identyfikator, przydatny do śledzenia albo blokowania pojedynczych tokenów

exp, nbf i iat są wartościami typu NumericDate, czyli liczbą sekund od 1 stycznia 1970 roku UTC. Są to sekundy, a nie milisekundy. Jeśli znacznik czasu dekoduje się na datę w roku 57 000, czytasz milisekundy i musisz podzielić przez tysiąc.

Wszystko inne w ładunku jest tym, co umieścił tam wystawca: role, uprawnienia, adres e-mail, identyfikator najemcy. Ponieważ prywatna nazwa oświadczenia mogłaby kiedyś zderzyć się z nazwą zarejestrowaną, wystawcy często opatrują własne nazwy przedrostkiem przypominającym adres URL.

Podpis

Podpis jest liczony z dokładnego tekstu dwóch pierwszych części połączonych kropką, z użyciem algorytmu z nagłówka i klucza. Zmień jeden znak nagłówka albo ładunku, a podpis przestaje pasować.

Przy HS256 ten sam sekret zarówno podpisuje, jak i weryfikuje, więc każdy, kto potrafi sprawdzić token, potrafi też go wytworzyć. Przy RS256 albo ES256 podpisuje klucz prywatny, a weryfikuje publiczny, i to właśnie pozwala dostawcy tożsamości wydawać tokeny, które wiele osobnych usług może zweryfikować, nie przechowując niczego tajnego.

Base64url to nie szyfrowanie

Base64url to ten sam pomysł co Base64, z dwiema różnicami: używa - i _ w miejsce + i /, dzięki czemu wynik jest bezpieczny w adresach URL, a końcowe dopełnienie = jest zwykle usuwane. Jest to kodowanie, czyli odwracalny sposób zapisu bajtów jako tekstu. Nie zapewnia absolutnie żadnej poufności.

Nigdy więc nie umieszczaj w ładunku tokenu JWT niczego wrażliwego: żadnych haseł, numerów kart, wewnętrznych notatek o użytkowniku. Zakładaj, że osoba trzymająca token oraz każdy, kto odczyta go z pliku logu albo z pamięci przeglądarki, widzi każde pole.

Istnieje osobny format, JWE, który faktycznie szyfruje ładunek i który ma pięć części zamiast trzech. Jeśli twój token ma trzy części, jest podpisany i czytelny.

Zdekodowanie to nie weryfikacja

To jest punkt, który ma znaczenie. Dekoder pokazuje, co token mówi. Nie mówi, czy token jest autentyczny. Weryfikacja jest osobnym krokiem i wymaga klucza.

Żeby naprawdę zaufać tokenowi, serwer musi:

  1. Sprawdzić podpis wobec właściwego klucza.
  2. Wymagać algorytmu, którego oczekuje, zamiast ufać polu alg w tokenie. Z pominięcia tego kroku biorą się dwa klasyczne ataki: ustawienie alg na none i usunięcie podpisu oraz wzięcie publicznego klucza RSA usługi i użycie go jako sekretu HMAC, przez co weryfikator RS256 zostaje podstępem zmuszony do wykonania HS256.
  3. Sprawdzić exp oraz, jeśli występuje, nbf, dopuszczając niewielki rozjazd zegarów.
  4. Sprawdzić, czy iss i aud zgadzają się z tym, czego oczekuje ta usługa.

Dopiero wtedy oświadczenia cokolwiek znaczą. Do tego momentu ładunek jest niezweryfikowanym ciągiem znaków, który przyszedł z sieci.

Uwagi praktyczne

  • Tokeny wysyła się zwykle w nagłówku HTTP jako Authorization: Bearer <token>, więc trzymaj ładunek małym. Każde dodane oświadczenie jest wysyłane przy każdym żądaniu.
  • Podpisany token pozostaje ważny aż do wygaśnięcia. Nie ma wbudowanego sposobu na jego unieważnienie i dlatego tokeny dostępu mają zwykle krótki czas życia, często liczony w minutach, a do uzyskiwania nowych służy osobny token odświeżający.
  • Jeśli token wygląda na uszkodzony, najpierw policz kropki. Dwie kropki i trzy części to normalny podpisany token JWT. Bardzo częstą przyczyną komunikatu "invalid signature" jest token ucięty przy wklejaniu z terminala.
  • Zdekodowanie tokenu w celu odczytania terminu ważności, podmiotu albo ról podczas diagnozowania jest całkowicie bezpieczne i nie wymaga żadnego sekretu. Dokładnie po to ładunek istnieje.