Dla programistów
Jak odczytać token JWT i co z niego wynika
Token JWT da się odczytać bez żadnego klucza, ale sam odczyt niczego nie dowodzi. Zobacz, co jest w trzech częściach tokenu, jak przeliczyć exp na datę i co musi sprawdzić serwer, zanim uwierzy w to, co token o sobie mówi.
- Redakcja HNarzędzi
- Publikacja:
JWT (JSON Web Token) to napis z trzech części oddzielonych kropkami. Dwie pierwsze to zakodowany JSON, więc w dekoderze JWT przeczytasz je bez żadnego klucza. Z odczytu dowiesz się, kto wystawił token, dla kogo jest przeznaczony i kiedy wygasa. Nie dowiesz się, czy ktoś go po drodze nie zmienił: to wymaga sprawdzenia podpisu.
Trzy części tokenu
Token ma postać nagłówek.payload.podpis. Format opisuje RFC 7515 (JWS), a zawartość payloadu RFC 7519.
| Część | Co zawiera | Zapis |
|---|---|---|
| Nagłówek (header) | obiekt JSON z algorytmem podpisu alg i typem typ |
base64url |
| Payload | obiekt JSON z claimami, czyli danymi o tokenie i użytkowniku | base64url |
| Podpis (signature) | wynik obliczeń na dwóch pierwszych częściach, dane binarne | base64url |
Nagłówek i payload zwykle zaczynają się od eyJ. JSON otwiera się znakami {", a te dwa znaki kodują się właśnie jako eyJ. To szybki sposób, żeby rozpoznać JWT w logu albo w nagłówku Authorization: Bearer ....
Przykład: token podpisany kluczem testowym
Przykład to syntetyczny token HS256, podpisany kluczem testowym test-secret-do-poradnika-jwt-2026. Wszystkie dane są zmyślone, a klucz służy tylko do prób. W produkcji klucz HS256 powinien być losowy: RFC 8725 (sekcja 3.5) zabrania używania zapamiętywalnych haseł jako klucza HMAC. Losowy sekret wygenerujesz w generatorze haseł, a o długości i sile haseł piszemy w poradniku Jak długie powinno być hasło i jak wygenerować bezpieczne.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0Iiwic3ViIjoidXNlci0xMDQyIiwiYXVkIjoiYXBpLmV4YW1wbGUudGVzdCIsIm5hbWUiOiJBbm5hIFdpxZtuaWV3c2thIiwicm9sZSI6ImVkaXRvciIsImlhdCI6MTc5MTY5ODQwMCwibmJmIjoxNzkxNjk4NDAwLCJleHAiOjE3OTE2OTkzMDB9.n4G2Adwwu0DTF37lNYw7zA4Bw_P_Z_6XB3_NmOvBOrY
Payload po zdekodowaniu:
{
"iss": "https://auth.example.test",
"sub": "user-1042",
"aud": "api.example.test",
"name": "Anna Wiśniewska",
"role": "editor",
"iat": 1791698400,
"nbf": 1791698400,
"exp": 1791699300
}
| Pole | Znaczenie |
|---|---|
iss |
wystawca tokenu |
sub |
podmiot, zwykle identyfikator użytkownika |
aud |
odbiorca, dla którego token jest przeznaczony |
name, role |
pola własne aplikacji, spoza standardu |
iat |
kiedy token wystawiono |
nbf |
przed tą chwilą token nie obowiązuje |
exp |
od tej chwili token jest wygasły |
Liczby w tym poradniku dotyczą niedzieli 11 października 2026, godziny 08:00 UTC (10:00 w Warszawie). Jeśli wkleisz token później, opisy względne w dekoderze będą inne i token będzie już wygasły.
Po wklejeniu tokenu i klucza testowego dekoder pokazuje status „Token wygasł”. Daty iat i nbf to 06:00:00 UTC, a exp to 06:15:00 UTC. Przy każdej z nich dekoder dopisuje „2 godziny temu” (od exp minęła dokładnie 1 godzina 45 minut). Podpis jest przy tym poprawny. Wygaśnięcie i podpis to dwa niezależne sprawdzenia, więc token może mieć jedno bez drugiego.

Base64url a Base64
Base64url to Base64 z dwiema zmianami: znak - zamiast + oraz _ zamiast / (RFC 4648, sekcja 5). W JWT odpada też dopełnienie = na końcu (RFC 7515, sekcja 2). Dzięki temu token mieści się w adresie URL i nagłówku HTTP bez znaków specjalnych. Część zwykłych dekoderów Base64 odrzuci taki ciąg, bo widzi w nim -, _ albo długość niepodzielną przez 4.
Konwerter Base64 przyjmuje oba warianty. W trybie „Dekoduj z Base64” wkleiliśmy do niego kolejne części tokenu:
- Nagłówek (36 znaków) dał
{"alg":"HS256","typ":"JWT"}, czyli 27 bajtów. - Payload (228 znaków) dał JSON z przykładu, 171 bajtów. Polskie znaki wracają poprawnie, bo konwerter odczytuje bajty jako UTF-8: „Wiśniewska” wraca z ogonkami.
- Nagłówek
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0(35 znaków, bez=) też został odczytany jako{"alg":"none","typ":"JWT"}. - Podpis (43 znaki, w tym
_) został przyjęty, ale konwerter zgłosił, że wynik nie jest tekstem UTF-8. To oczekiwane: podpis HMAC-SHA256 to 32 bajty binarne.
W drugą stronę działa opcja „Wariant URL-safe”. Tekst {"alg":"none","typ":"JWT"} ma 26 bajtów, więc zwykły Base64 daje eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=, a wariant URL-safe ten sam ciąg bez =.
Podpis liczy się z dokładnie tych znaków, które stoją w tokenie (RFC 7515, sekcja 2, „JWS Signing Input”). Jeśli zdekodujesz JSON, zmienisz w nim odstępy albo kolejność pól i zakodujesz go z powrotem, dostaniesz inny ciąg, a podpis przestanie pasować.
Payload odczytasz też z konsoli, bez narzędzi online (Node.js 16 lub nowszy, token w zmiennej TOKEN):
node -e "console.log(Buffer.from(process.argv[1].split('.')[1], 'base64url').toString())" "$TOKEN"
Czas w tokenie: exp, iat, nbf
exp, iat i nbf to liczby sekund od 1 stycznia 1970, godziny 00:00:00 UTC (RFC 7519, sekcja 2, NumericDate). Mogą być ułamkowe i nie zawierają strefy czasowej. Reguły z sekcji 4.1.4-4.1.6:
exp: od tej chwili token nie może być przyjęty. W chwiliexpjest już wygasły.nbf: przed tą chwilą token nie może być przyjęty.iat: moment wystawienia, służy do ustalenia wieku tokenu. Sam nie decyduje o ważności.
Dekoder stosuje te same reguły. Przeliczenie liczby na datę robi konwerter Unix timestamp. Dla exp z przykładu, 1791699300, pokazuje 06:15:00 UTC i 08:15:00 czasu lokalnego (Europe/Warsaw, w październiku UTC+2).

Strefy czasowe
Token zawsze niesie moment w UTC. Strefa pojawia się dopiero przy wyświetlaniu, więc ten sam exp to 06:15 w logu serwera ustawionym na UTC i 08:15 w przeglądarce w Warszawie. Dwie godziny różnicy nie oznaczają błędu. Zmiana czasu z letniego na zimowy nie zmienia liczby sekund w tokenie, zmienia tylko zapis lokalny. Tabela w dekoderze podaje daty w UTC. Żeby zobaczyć ten sam moment w swojej strefie, wklej liczbę do konwertera: pokazuje czas lokalny i UTC w osobnych wierszach.
Sekundy, nie milisekundy
Poprawny exp ma dziś 10 cyfr. Jeśli ma 13, ktoś wpisał milisekundy: Date.now() w JavaScripcie i System.currentTimeMillis() w Javie zwracają milisekundy, a JWT wymaga sekund (Math.floor(Date.now() / 1000)). Konwerter wykrywa jednostkę po wielkości liczby: dla 1791709200000 wskazuje milisekundy i 11:00:00 w Warszawie. Biblioteka, która potraktuje tę liczbę jako sekundy, odczyta rok około 58 700 i token praktycznie nigdy nie wygaśnie. Dekoder też to wychwytuje: przy takiej wartości pokazuje datę w roku 58747 i ostrzeżenie, że wartość wygląda na milisekundy.
Przesunięcie zegara
Wyobraź sobie serwer logowania, którego zegar spieszy się o 30 sekund. Wystawia token z iat i nbf równymi 08:00:30, a serwer API ma w tej chwili 08:00:00. Dla takiego tokenu dekoder pokazuje status „Token jeszcze nie obowiązuje (nbf)”, a nbf „za 30 sekund”. W praktyce świeżo wystawiony token jest odrzucany przez pierwsze pół minuty i błąd znika sam. Dekoder porównuje czas dokładnie, bez tolerancji.
RFC 7519 pozwala bibliotekom przyjąć niewielki margines, zwykle nie większy niż kilka minut. W dokumentacji biblioteki szukaj opcji o nazwie w rodzaju leeway albo clockTolerance. Zacznij jednak od zsynchronizowania zegarów (NTP), bo duży margines wydłuża czas, w którym wygasły token jest jeszcze przyjmowany.
Sama wartość iat z przyszłości nie unieważnia tokenu: dekoder pokaże „za 30 sekund” i status „ważny”. Traktuj to jako wskazówkę, że zegary się rozjechały.
Pole alg w nagłówku
alg mówi, jakim algorytmem podpisano token. HS256, HS384 i HS512 używają jednego sekretu (HMAC), więc ten, kto potrafi sprawdzić podpis, potrafi też go złożyć. RS, PS i ES (256, 384 i 512) używają pary kluczy: podpisuje klucz prywatny, a sprawdza publiczny.
Dekoder sprawdza podpis dla wszystkich tych algorytmów. Dla HS wpisujesz sekret, dla pozostałych klucz publiczny w formacie PEM („BEGIN PUBLIC KEY”) albo JWK. Certyfikatów X.509 i kluczy w formacie „RSA PUBLIC KEY” (PKCS#1) dekoder nie obsługuje. Klucz publiczny nie jest tajny, więc sprawdzanie tokenów RS i ES w narzędziu online nie ujawnia nic wrażliwego. Sekretu HS z produkcji nie wpisuj nigdzie poza własnym serwerem.
Odczytanie to nie weryfikacja podpisu
Weźmy token z przykładu i zmieńmy w payloadzie "role": "editor" na "role": "admin", zostawiając stary podpis:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0Iiwic3ViIjoidXNlci0xMDQyIiwiYXVkIjoiYXBpLmV4YW1wbGUudGVzdCIsIm5hbWUiOiJBbm5hIFdpxZtuaWV3c2thIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzkxNjk4NDAwLCJuYmYiOjE3OTE2OTg0MDAsImV4cCI6MTc5MTY5OTMwMH0.n4G2Adwwu0DTF37lNYw7zA4Bw_P_Z_6XB3_NmOvBOrY
Dekoder pokaże "role": "admin" i datę wygaśnięcia, jak dla każdego innego tokenu. Dopiero po wpisaniu klucza testowego pojawi się komunikat „Podpis niepoprawny”: klucz jest inny albo token został zmieniony. Bez klucza dekoder nie ma jak odróżnić tokenu oryginalnego od podmienionego, a pod wynikiem przypomina, że dekodowanie nie weryfikuje podpisu.
Serwer, który przyjmuje token, powinien kolejno:
- Sprawdzić podpis algorytmem z własnej listy dozwolonych, a nie tym, który wskazuje nagłówek tokenu (RFC 8725, sekcja 3.1).
- Sprawdzić
expinbf. - Sprawdzić wystawcę
iss(sekcja 3.8) i odbiorcęaud(sekcja 3.9). - Dopiero potem korzystać z
sub,rolei pozostałych pól.
Aplikacja w przeglądarce może odczytać payload, żeby wyświetlić imię użytkownika. Decyzji o uprawnieniach nie opieraj na tym odczycie, bo użytkownik może zmienić wszystko, co jego przeglądarka widzi. Rolę musi sprawdzić serwer.
Dlaczego payload nie jest szyfrowany
Zwykły JWT jest podpisany, a nie zaszyfrowany. Podpis chroni przed zmianą, a nie przed odczytem: base64url to sposób zapisu, nie szyfr. Każdy, kto ma token (pośredni serwer, log, osoba z dostępem do pliku HAR), przeczyta imię i rolę tak samo jak w przykładzie.
RFC 7519 (sekcja 12) wskazuje trzy możliwości: zaszyfrowany JWT, przesyłanie wyłącznie kanałem szyfrowanym z uwierzytelnieniem odbiorcy (TLS) i pominięcie wrażliwych danych, które jest najprostsze. Nie umieszczaj w tokenie haseł, numerów PESEL, numerów kart ani danych zdrowotnych.
Zaszyfrowany token (JWE) ma pięć części zamiast trzech. Dekoder rozpoznaje go po liczbie kropek i pokazuje, że zawartości nie da się odczytać bez klucza deszyfrującego.
Czego nie wklejać do narzędzi online
Token JWT działa jak hasło tymczasowe: kto go ma, może się nim posłużyć do chwili exp. Kilka zasad:
- Nie wklejaj tokenów produkcyjnych do stron, których nie kontrolujesz. Dekoder HNarzędzi działa w przeglądarce. W naszym teście po wklejeniu tokenu i klucza przeglądarka nie wysłała żadnego żądania sieciowego, a ty możesz to sprawdzić w zakładce „Sieć” (Network) narzędzi deweloperskich. Dotyczy to jednak tylko tej strony, nie innych dekoderów.
- Do prób używaj tokenów ze środowiska testowego albo własnych, jak w tym poradniku.
- Token produkcyjny odczytaj lokalnie, na przykład poleceniem z poprzedniej sekcji.
- Sekretu HMAC z produkcji nie wpisuj do żadnego narzędzia online. Przy algorytmach RS i ES wystarczy klucz publiczny.
- Zanim wyślesz zrzut ekranu, plik HAR, log albo zgłoszenie błędu, usuń nagłówek
Authorizationi tokeny z adresów URL. Jeśli musisz pokazać token, podaj tylko nagłówek i payload, bez podpisu, i zamaskuj dane osobowe. - Token, który trafił w niewłaściwe miejsce, unieważnij u wystawcy (wylogowanie sesji, odwołanie tokenu). Przy wycieku klucza podpisującego trzeba wymienić klucz. Krótki
expogranicza skutki.
Typowe błędy
| Objaw | Najczęstsza przyczyna | Co sprawdzić |
|---|---|---|
| Odpowiedź 401 po jakimś czasie od logowania | minął exp |
exp w dekoderze, odnowienie tokenu albo ponowne logowanie |
| Świeży token odrzucany przez pierwsze sekundy | nbf lub iat w przyszłości, zegary wystawcy i serwera się różnią |
status „nie obowiązuje” w dekoderze, synchronizacja zegarów |
| Token nigdy nie wygasa | exp w milisekundach (13 cyfr) |
ostrzeżenie o milisekundach w dekoderze, konwerter Unix timestamp, dzielenie przez 1000 |
| „Podpis niepoprawny” | zły sekret lub klucz, spacja albo nowa linia w sekrecie, sekret zakodowany w base64 (włącz opcję „Sekret jest zakodowany w base64url”), token zmieniony po podpisaniu | sekret skopiowany bez białych znaków, ten sam klucz po obu stronach |
| „To nie wygląda na JWT” | token ucięty w logu albo brakuje kropki | trzy części oddzielone kropkami; dekoder sam usuwa prefiks Bearer, cudzysłowy i białe znaki |
| „Nagłówek lub payload nie jest poprawnym obiektem JSON” | zdekodowany tekst nie jest JSON | część zdekodowana w konwerterze Base64, poprawki według poradnika Błąd w JSON |
| Pięć części zamiast trzech | to token JWE, zaszyfrowany | klucz deszyfrujący u odbiorcy, dekoder go nie odczyta |
Token z alg none
RFC 7519 (sekcja 6) dopuszcza token niezabezpieczony: alg ma wartość none, a podpis jest pustym napisem, więc token kończy się kropką. To bywa uzasadnione, gdy zawartość chroni coś innego, na przykład TLS (RFC 8725, sekcja 3.2).
Problem zaczyna się wtedy, gdy serwer ufa nagłówkowi. Atakujący zmienia alg na none, usuwa podpis i wpisuje, co chce. Część bibliotek uznawała taki token za poprawny (RFC 8725, sekcja 2.1; tam też opisano podobny atak, w którym RS256 zamieniono na HS256, a klucz publiczny użyto jako sekretu HMAC). Obrona to lista algorytmów ustalona po stronie serwera (sekcja 3.1) i biblioteka, która nie przyjmuje none, jeśli nie poprosisz o to wprost (sekcja 3.2).
Dla tokenu z nagłówkiem {"alg":"none","typ":"JWT"} i rolą admin dekoder pokazuje ostrzeżenie (Algorytm „none” oznacza token bez podpisu. Nie ufaj mu.), a weryfikacja zgłasza, że algorytm nie jest obsługiwany.
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0Iiwic3ViIjoidXNlci0xMDQyIiwiYXVkIjoiYXBpLmV4YW1wbGUudGVzdCIsIm5hbWUiOiJBbm5hIFdpxZtuaWV3c2thIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzkxNzA1MzAwLCJuYmYiOjE3OTE3MDUzMDAsImV4cCI6MTc5MTcwODkwMH0.
Źródła
Stan na październik 2026.
- RFC 7519: JSON Web Token (JWT), sekcje 2 (NumericDate), 4.1.4-4.1.6 (
exp,nbf,iat), 6 (token niezabezpieczony) i 12 (prywatność). - RFC 7515: JSON Web Signature (JWS), sekcja 2 (base64url) i 5.2 (sprawdzanie podpisu).
- RFC 4648: Base-N Encodings, sekcja 5 (Base64 z alfabetem bezpiecznym dla URL).
- RFC 8725: JSON Web Token Best Current Practices, sekcje 2.1, 3.1, 3.2, 3.5, 3.8 i 3.9.