Wat er werkelijk in een JSON Web Token zit
Een JSON Web Token (JWT) bestaat uit drie stukken tekst die door punten aan elkaar zitten. Elk stuk is Base64url-gecodeerd, en de eerste twee zijn gewoon JSON. Iedereen die de token heeft, kan lezen wat erin staat. Niets in een gewone JWT is versleuteld.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0 <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW <- signature
Met punten aaneengeregen is dat de token die je in een verzoek plakt. Decodeer de eerste twee delen en je krijgt gewone JSON:
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }
Een echte payload draagt meestal ook tijdstempels en een doelgroep:
{
"iss": "https://auth.example.com",
"sub": "12345",
"aud": "api.example.com",
"iat": 1756000000,
"exp": 1756003600,
"roles": ["editor"]
}
De header
De header beschrijft hoe de token ondertekend is. Twee velden tellen het zwaarst:
algnoemt het ondertekeningsalgoritme, bijvoorbeeldHS256(HMAC met SHA-256, met een gedeeld geheim) ofRS256(RSA-handtekening met SHA-256, ondertekend met een privésleutel en gecontroleerd met een publieke).kid, het sleutel-ID, is optioneel en vertelt de ontvanger welke van meerdere sleutels gebruikt is, zodat sleutels vervangen kunnen worden zonder bestaande tokens te breken.
typ is meestal gewoon JWT en heeft geen betekenis voor de beveiliging.
De payload en zijn claims
De payload is een JSON-object met claims, wat simpelweg het woord van de specificatie is voor uitspraken over het onderwerp van de token. Sommige claimnamen zijn door de standaard geregistreerd, en ze zijn allemaal kort:
| Claim | Naam | Wat hij bevat |
|---|---|---|
iss | Issuer | Wie de token heeft aangemaakt, meestal een URL of dienstnaam |
sub | Subject | Over wie de token gaat, meestal een stabiel gebruikers-ID |
aud | Audience | Voor wie de token bedoeld is, zodat een dienst tokens voor een andere dienst kan weigeren |
exp | Expiration time | Het moment waarna de token geweigerd moet worden |
nbf | Not before | Het moment waarvóór de token geweigerd moet worden |
iat | Issued at | Wanneer de token is aangemaakt |
jti | JWT ID | Een unieke identifier, handig om afzonderlijke tokens te volgen of te blokkeren |
exp, nbf en iat zijn allemaal NumericDate-waarden, oftewel seconden sinds 1 januari 1970 UTC. Het zijn seconden, geen milliseconden. Als een tijdstempel decodeert naar een datum in het jaar 57.000, lees je milliseconden en moet je door duizend delen.
Al het overige in de payload is wat de uitgever erin heeft gezet: rollen, rechten, een e-mailadres, een tenant-ID. Omdat een eigen claimnaam ooit met een geregistreerde zou kunnen botsen, geven uitgevers hun eigen claims vaak een namespace met een voorvoegsel in de vorm van een URL.
De handtekening
De handtekening wordt berekend over de exacte tekst van de eerste twee delen aaneengeregen met een punt, met het algoritme uit de header en een sleutel. Verander één teken van de header of payload en de handtekening klopt niet meer.
Bij HS256 ondertekent en verifieert hetzelfde geheim, dus wie een token kan controleren kan er ook een aanmaken. Bij RS256 of ES256 ondertekent een privésleutel en verifieert een publieke sleutel, en dat is wat een identiteitsprovider in staat stelt tokens uit te delen die veel losse diensten kunnen valideren zonder iets geheims in handen te hebben.
Base64url is geen encryptie
Base64url is hetzelfde idee als Base64, met twee verschillen: het gebruikt - en _ in plaats van + en / zodat het resultaat veilig is in URL's, en de aanvulling met = aan het eind wordt meestal weggelaten. Het is een codering, een omkeerbare manier om bytes als tekst te schrijven. Het biedt geen enkele vertrouwelijkheid.
Zet dus nooit iets gevoeligs in een JWT-payload: geen wachtwoorden, geen kaartnummers, geen interne aantekeningen over de gebruiker. Ga ervan uit dat degene die de token heeft, en iedereen die hem uit een logbestand of browseropslag leest, elk veld kan zien.
Er bestaat een apart formaat, JWE, dat de payload wél versleutelt, en dat heeft vijf delen in plaats van drie. Heeft je token drie delen, dan is hij ondertekend en leesbaar.
Decoderen is niet verifiëren
Dit is het punt dat ertoe doet. Een decoder laat je zien wat een token zegt. Hij vertelt je niet of de token echt is. Verificatie is een aparte stap en heeft de sleutel nodig.
Om een token echt te vertrouwen, moet een server:
- De handtekening controleren tegen de juiste sleutel.
- Het algoritme afdwingen dat hij verwacht, in plaats van op het veld
algin de token te vertrouwen. Twee klassieke aanvallen komen voort uit het overslaan hiervan:algopnonezetten en de handtekening weglaten, en de publieke RSA-sleutel van een dienst pakken en die als HMAC-geheim gebruiken, zodat een verifier voorRS256verleid wordt omHS256uit te voeren. expcontroleren en, als hij aanwezig is,nbf, met een kleine marge voor klokafwijking.- Controleren of
issenaudovereenkomen met wat deze dienst verwacht.
Pas dan betekenen de claims iets. Tot dat moment is de payload een ongeverifieerde string die over het netwerk is aangekomen.
Praktische opmerkingen
- Tokens worden meestal in een HTTP-header verstuurd als
Authorization: Bearer <token>, dus houd de payload klein. Elke claim die je toevoegt gaat bij elk verzoek mee. - Een ondertekende token blijft geldig tot hij verloopt. Er is geen ingebouwde manier om er een in te trekken, en daarom hebben access tokens meestal een korte levensduur, vaak minuten, met een aparte refresh token om nieuwe te halen.
- Ziet een token er misvormd uit, tel dan eerst de punten. Twee punten en drie delen is een normale ondertekende JWT. Een afgekapte token die uit een terminal geplakt is, is een veelvoorkomende oorzaak van "invalid signature".
- Een token decoderen om tijdens het debuggen zijn vervaltijd, zijn onderwerp of zijn rollen te lezen is volstrekt veilig en vereist geen geheim. Daar is de payload precies voor.