JWT-beveiliging: aanvallen, verdediging en een checklist voor 2026
JSON Web Tokens vormen de basis van vrijwel alle moderne authenticatie, en toch worden de best practices die ze veilig houden veel vaker overgeslagen dan goed is. JWT is het de-factoformaat voor credentials in OAuth 2.0, OpenID Connect en service-to-service-aanroepen binnen microservices. Het is ook de bron van een gestage stroom CVE’s, jaar in jaar uit. Bijna allemaal komen ze terug op dezelfde vermijdbare fouten: ongetekende tokens accepteren, het algoritme vertrouwen dat een aanvaller koos, een zwak ondertekeningsgeheim gebruiken of claimvalidatie overslaan.
Een JWT is veilig wanneer vier dingen tegelijk kloppen. De handtekening is intact, het algoritme kan niet door een aanvaller worden verwisseld, de claims worden daadwerkelijk gecontroleerd, en het token wordt opgeslagen op een plek waar het niet eenvoudig te stelen is. Breek er één en je hebt een authenticatie-bypass, geen geharde API. Deze gids loopt eerst langs de drie aanvallen die het meest uitmaken en daarna langs de verdediging: het juiste algoritme kiezen en vastleggen, sleutels beheren, claims valideren en tokens veilig opslaan. Aan het eind staat een checklist die je rechtstreeks in een review kunt plakken.
Hoe een JWT-handtekening je écht beschermt (en wat ze niet doet)
Voordat een aanval ergens op slaat, moet één feit helder zijn: een JWT is gecodeerd, niet versleuteld. Een ondertekend token heeft drie Base64URL-segmenten, gescheiden door punten: header.payload.signature. De header en payload zijn gewoon Base64URL van JSON. Iedereen die het token in handen heeft, kan elke claim erin lezen. Plak een willekeurig token in onze JWT-decoder en je ziet de header en payload uitgeschreven in leesbare JSON, zonder dat er een sleutel nodig is. De payload is publiek, en zo is het bedoeld.
Waar komt de beveiliging dan vandaan? Uit de handtekening, en uitsluitend de handtekening. Het is een cryptografische waarde, berekend over de header en payload met een geheim (HMAC) of een privésleutel (RSA, ECDSA). Een aanvaller kan een token vrij lezen, maar kan geen ander token maken dat de verificatie doorstaat zonder de ondertekeningssleutel. Die ene eigenschap is het hele vertrouwensmodel.
Daar volgen twee gevolgen uit. Ten eerste: zet nooit geheimen in de payload (wachtwoorden, API-sleutels, volledige persoonsgegevens), want die is leesbaar voor iedereen die het token onderschept. Ten tweede: je hele beveiligingshouding leunt op één stap, namelijk de handtekening correct verifiëren. En precies die stap nemen aanvallers onder vuur. Wil je een uitgebreidere uitleg over het segment-voor-segment lezen van tokens, zie dan hoe je een JWT decodeert.
De 3 kritieke JWT-aanvallen (en hoe je ze elk stopt)
De meeste JWT-kwetsbaarheden zijn variaties op één thema: de server vertrouwt iets wat de aanvaller in handen heeft. Hier zijn de drie die authenticatie regelrecht onderuithalen, met het mechanisme achter elk en de oplossing.
1. De alg:none-aanval: bypass via ongetekend token
De JWS-spec kent een alg-waarde none, oftewel “ongetekend”. Een alg:none-token heeft een leeg handtekeningsegment en eindigt nog steeds op een afsluitende punt, zoals header.payload.. De aanval is simpel: neem een geldig token, zet de alg in de header op none, vul de claims in die je wilt (zeg "role": "admin") en laat de handtekening weg. Vroege JWT-libraries accepteerden dit standaard, dus het vervalste token gleed zo door de verificatie heen. Geen sleutel, geen ondertekening, volledige impersonatie.
Je ziet hoe zo’n token eruitziet door het “alg:none”-voorbeeld te laden in onze JWT-decoder, die een expliciete rode waarschuwing toont dat het token ongetekend is en nooit voor authenticatie mag worden geaccepteerd. Er zelf eentje namaken is een oefening van een minuut die de dreiging meteen invoelbaar maakt.
De verdediging is een expliciete algoritme-allowlist bij elke verify-aanroep. Laat de library nooit standaard bepalen wat acceptabel is, want oudere standaardinstellingen waren te toegeeflijk en de prijs om expliciet te zijn is één extra optie.
// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);
// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });
none hoort nooit in die array thuis. Kan jouw library geen algoritmen vastleggen, vervang hem dan.
2. Algoritmeverwarring: RS256 gedegradeerd naar HS256
Dit is in de praktijk de gevaarlijkste JWT-kwetsbaarheid, bekend sinds 2015 en vandaag de dag nog steeds aangetroffen in audits. Het misbruikt servers die hoe ze verifiëren laten afhangen van het alg-veld in de header, het ene deel van het token dat een aanvaller kan herschrijven.
Zo werkt het mechanisme. Je server geeft RS256-tokens uit: hij ondertekent met een RSA-privésleutel en verifieert met de bijbehorende publieke sleutel. Die publieke sleutel is per definitie publiek: hij staat misschien in je JWKS-endpoint of in je repo. De aanvaller pakt hem, verandert de tokenheader van RS256 naar HS256, en ondertekent een vervalste payload met HMAC-SHA256, waarbij hij de string van de publieke sleutel als HMAC-geheim gebruikt. En dan de verificatiekant: als jouw code alg uit de header leest en op basis daarvan HMAC kiest, berekent hij HMAC-SHA256 over het token met diezelfde publieke sleutel als geheim. De handtekeningen komen overeen. Het vervalste token wordt geaccepteerd.
De grondoorzaak is dat twee feiten op elkaar botsen: de verificateur vertrouwde de door de aanvaller bepaalde alg-header, en de publieke RSA-sleutel lag voor de aanvaller klaar om als HMAC-sleutel te gebruiken. Geen van beide feiten is op zichzelf een bug. Een publieke sleutel hoort publiek te zijn, en een alg-header hoort het token te beschrijven. Het misgaat pas wanneer je verificatielogica die header laat bepalen welk sleuteltype en algoritme worden gebruikt, want dan stuurt een waarde die de aanvaller schrijft het cryptopad dat de server doorloopt.
// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);
// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Leg het asymmetrische algoritme expliciet vast (alleen RS256 of ES256), houd HMAC-verificatie op een volledig apart codepad ten opzichte van RSA-verificatie, en gebruik een onderhouden library die sleuteltypen uit elkaar houdt. Onze JWT-decoder markeert elk token uit de HS-familie met een waarschuwing voor publieke-sleutelverwarring, juist omdat deze aanval zo vaak voorkomt. Duikt een token dat je asymmetrisch verwachtte als HS256 op, dan is die waarschuwing jouw signaal.
3. Zwak HMAC-geheim: brute-force- en woordenboekaanvallen
Wanneer je wél HMAC gebruikt (HS256/384/512), berust de hele beveiliging van het token op de entropie van één geheim. Is dat geheim kort, een woordenboekwoord, of een waarde als secret of password123, dan kan een aanvaller die één geldig token bemachtigt het offline kraken. Tools als hashcat banen zich een weg door miljarden kandidaten per seconde tegen de handtekening van het token. Zodra het geheim valt, kan de aanvaller elk gewenst token aanmaken, inclusief geldige admin-credentials, en dat blijft zo.
Wat deze aanval zo stilletjes gevaarlijk maakt, is dat hij volledig offline verloopt. De aanvaller bestookt je login-endpoint niet, dus er is geen rate limit die afgaat en niets in je logs dat opvalt. Hij bemachtigt één token, kraakt het geheim op zijn eigen hardware, en komt pas terug wanneer hij tokens kan ondertekenen die elke controle doorstaan die je hebt. Hier is de oplossing eenduidig: gebruik minstens 32 willekeurige bytes (256 bits) uit een cryptografisch veilige bron, en bewaar ze in een secrets manager, nooit in code of een repo.
// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";
// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');
Snel een sterke waarde nodig? Onze wachtwoordgenerator levert strings met hoge entropie die geschikt zijn als HMAC-sleutel. Wil je het verschil in de praktijk voelen? Onderteken een testtoken met een sterk geheim in onze JWT-encoder, die volledig in de browser draait, zodat het geheim je machine nooit verlaat. En zodra verificatie een vertrouwensgrens overschrijdt, met meerdere services of externe verificateurs, stop dan helemaal met HS256 en stap over op een asymmetrisch algoritme. Dat komt hierna aan bod.
Het juiste algoritme kiezen en vastleggen
Bij de algoritmekeuze wordt de verwarringsaanval gewonnen of verloren, dus kies bewust. De drie die je daadwerkelijk gebruikt:
| Algoritme | Type | Sleutel voor ondertekenen / verifiëren | Wanneer gebruiken |
|---|---|---|---|
| HS256 | Symmetrisch (HMAC) | Eén gedeeld geheim | Eén vertrouwensgrens, dezelfde partij tekent en verifieert |
| RS256 | Asymmetrisch (RSA) | Privésleutel tekent / publieke sleutel verifieert | Cross-service, externe verificatie, JWKS-rotatie |
| ES256 | Asymmetrisch (ECDSA) | Privésleutel tekent / publieke sleutel verifieert | Hetzelfde als RS256, met kleinere en snellere sleutels; voorkeur voor nieuwe systemen |
De regel is kort. Tekent en verifieert dezelfde partij binnen één vertrouwensgrens, dan is HS256 prima en snel. Moet iemand anders dan de ondertekenaar verifiëren, zoals een andere service, een partner of een publieke client, gebruik dan een asymmetrisch algoritme, en geef de voorkeur aan ES256: de sleutels en handtekeningen zijn veel kleiner dan bij RSA met gelijke sterkte. Je kunt HS256-, RS256- en ES256-tokens naast elkaar ondertekenen in de JWT-encoder om hun structuur en handtekeninglengte te vergelijken.
Wat je ook kiest, de verdediging die er echt toe doet, blijft dezelfde: leg één expliciete set algoritmen vast bij de verify-aanroep en vertrouw nooit het alg-veld in de header. De allowlist is het fundament waar al het andere op rust.
Sleutelbeheer en -rotatie
Algoritmen zijn niet veiliger dan de sleutels erachter, en juist over sleutelbeheer zwijgen de meeste gidsen. Voor HS256 is het geheim minstens 32 willekeurige bytes en woont het in een secrets manager, zoals AWS Secrets Manager, HashiCorp Vault of Azure Key Vault. Voor asymmetrische algoritmen hoort de privésleutel thuis in een HSM of KMS en raakt hij nooit de applicatiecode; de publieke sleutel wordt gepubliceerd, doorgaans via een JWKS-endpoint dat verificateurs ophalen.
Rotatie moet routine zijn, geen noodgeval. Voorzie elke sleutel van een kid (key ID) in de JWT-header, zodat verificateurs weten welke sleutel een bepaald token heeft ondertekend. Houd aan de verificatiekant een kleine set geldige sleutels aan, de huidige plus de meest recente vorige, zodat tokens die net vóór een rotatie zijn ondertekend tijdens hun levensduur nog verifiëren. Die overlap maakt rotatie soepel in plaats van een storing.
Een korte checklist voor sleutels:
- Roteer ondertekeningssleutels minstens elke 90 dagen, en onmiddellijk bij elke verdenking van compromittering.
- Publiceer publieke sleutels via JWKS; versioneer ze met
kid. - Houd privésleutels en HMAC-geheimen in een KMS of HSM, dus nooit in git, nooit in clientcode, nooit hardgecodeerd.
- Roteer bij een lek meteen de sleutel en trek uitstaande refresh-tokens in.
Claimvalidatie die je niet mag overslaan
De handtekeningcontrole bewijst dat een token authentiek is. Ze bewijst niet dat het token voor jou is, op dit moment. Dat is de taak van claimvalidatie, en het is de goedkoopste verdediging die je kunt toevoegen. Vijf claims moeten bij elke aanvraag worden gecontroleerd:
exp(expiration): weiger tokens waarvan de vervaltijd in het verleden ligt.nbf(not before): weiger tokens die worden gebruikt voordat hun geldige venster opengaat.iat(issued at): weiger desgewenst tokens die onwaarschijnlijk oud zijn.iss(issuer): bevestig dat het token afkomstig is van de uitgever die je vertrouwt.aud(audience): bevestig dat het token is aangemaakt voor jouw service. Een ontbrekendeaud-controle is het meest voorkomende stille gat: een token dat voor de ene API is uitgegeven, kan dan tegen een andere worden afgespeeld.
De meeste libraries valideren deze voor je wanneer je de verwachte waarden meegeeft:
jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5, // seconds, for distributed clock skew
});
Sta een kleine kloktolerantie toe (vijf seconden is gebruikelijk), zodat lichte drift tussen servers verder geldige tokens niet weigert. Weersta de neiging om die ruimer te maken; een royale tolerantie verbreedt het venster waarin een verlopen token nog werkt, en dat is precies wat exp moet sluiten. Wil je de exp- en iat-waarden op een token met het blote oog nalopen, gooi het dan in de JWT-decoder en reken de timestamps om met onze Unix-timestampomrekenaar.
Levensduur van tokens en waar je JWT’s opslaat
Controles aan de serverkant zijn maar de helft van het verhaal. Waar de client het token bewaart, bepaalt hoe makkelijk het te stelen is, en bij de opslag komen XSS en sessiekaping samen. Het patroon dat standhoudt: een kortlevend access-token (15 tot 60 minuten), gekoppeld aan een apart, langerlevend refresh-token dat intrekbaar is.
Bij de opslag gaat het om één afweging:
| Opslaglocatie | XSS-blootstelling | CSRF-risico | Aanbeveling |
|---|---|---|---|
| localStorage | Hoog: elke JavaScript op de pagina kan het lezen | Geen | Vermijden voor sessietokens |
| HttpOnly + Secure + SameSite=Strict cookie | Laag: onzichtbaar voor JavaScript | Vereist CSRF-bescherming | Aanbevolen voor sessies |
Een token in localStorage is leesbaar door elk script dat op de pagina draait, dus één XSS-bug lekt de hele sessie, en de aanvaller kan die vanaf zijn eigen machine afspelen zolang hij geldig is. Een HttpOnly-cookie kan helemaal niet door JavaScript worden gelezen, wat de schade van XSS beperkt tot wat een aanvaller binnen een live pagina kan doen: vervelend, maar geen gestolen credential dat hij kan meenemen. De prijs van de cookieaanpak is dat je nu CSRF-bescherming nodig hebt, omdat cookies bij elke aanvraag automatisch meeliften; SameSite=Strict plus een CSRF-token vangt dat op. Houd het access-token kort, zodat een lek een kleine straal heeft, en zet het refresh-token in een HttpOnly-, Secure-, SameSite-cookie. Trek bij uitloggen of vermoede compromittering het refresh-token aan de serverkant in en roteer de ondertekeningssleutel. Voor de bredere context rond XSS, CSRF en veilige cookies, zie onze gids best practices voor webbeveiliging.
JWT-beveiligingschecklist
Loop dit door voordat je welke JWT-gebaseerde authenticatie dan ook uitrolt:
- Verificatie legt een expliciete algoritme-allowlist vast en weigert
alg:none. - Asymmetrische verificatie codeert het verwachte algoritme hard en leest nooit
alguit de header (blokkeert verwarring). - HS256-geheimen zijn minstens 32 willekeurige bytes, geladen uit een KMS.
- Privésleutels wonen in een HSM/KMS; publieke sleutels worden via JWKS gepubliceerd en met
kidgeversioneerd. - Ondertekeningssleutels roteren minstens elke 90 dagen.
- Elke aanvraag valideert
exp,nbf,iat,issenaud, met een kloktolerantie van 5 seconden of minder. - Access-tokens leven 15 tot 60 minuten; refresh-tokens wonen in een
HttpOnly-cookie. - Geen geheimen in de payload, want die is gecodeerd, niet versleuteld.
FAQ
Is JWT standaard veilig?
Nee. JWT-beveiliging hangt af van de configuratie. Je moet het algoritme vastleggen, alg:none weigeren, een geheim of sleutel met hoge entropie gebruiken en claims valideren. Standaard- of toegeeflijke library-instellingen staan vaak authenticatie-bypasses toe.
Wat is de gevaarlijkste JWT-kwetsbaarheid?
Algoritmeverwarring, waarbij RS256 wordt gedegradeerd naar HS256 en de publieke sleutel als HMAC-geheim wordt gebruikt. Ze is bekend sinds 2015 en duikt nog steeds op in audits, omdat ze servers misbruikt die de verificatiemethode kiezen op basis van de alg in de header.
Moet ik HS256 of RS256 gebruiken?
Gebruik HS256 wanneer dezelfde partij tekent en verifieert binnen één vertrouwensgrens. Gebruik RS256 of ES256 wanneer een andere service of een derde partij moet verifiëren, of wanneer je JWKS-rotatie nodig hebt. Geef voor nieuwe systemen de voorkeur aan ES256: kleinere, snellere sleutels bij gelijke sterkte.
Waar moet ik een JWT opslaan?
Geef de voorkeur aan een HttpOnly-, Secure-, SameSite-cookie voor sessietokens, omdat JavaScript die niet kan lezen en één XSS-bug ze niet kan stelen. Vermijd localStorage voor sessietokens, want elke XSS lekt de hele sessie en maakt afspelen mogelijk.
Hoe vaak moet ik JWT-ondertekeningssleutels roteren?
Roteer als routine minstens elke 90 dagen, en onmiddellijk bij elk vermoeden van compromittering. Versioneer sleutels met kid en houd zowel de actieve sleutel als de meest recente vorige op de verificateur, zodat tokens die net vóór de rotatie zijn ondertekend nog valideren.
Kan een JWT worden gemanipuleerd?
Niet zonder de ondertekeningssleutel; geen enkele aanvaller kan een token vervalsen dat de verificatie doorstaat. Maar als je server alg:none accepteert, kwetsbaar is voor algoritmeverwarring of een zwak geheim gebruikt, kan de handtekening worden omzeild. Dat zijn configuratiefouten, geen tekortkomingen in JWT zelf.
Welke claims moet ik valideren?
Valideer exp (expiration), nbf (not before), iat (issued at), iss (issuer) en aud (audience). Een ontbrekende aud-controle is de meest voorkomende stille kwetsbaarheid: een token bedoeld voor de ene service kan tegen een andere worden afgespeeld.
Conclusie
JWT-beveiliging is niet ingewikkeld, maar elke laag moet standhouden. De handtekening is je enige garantie, dus verifieer haar correct. Leg één expliciet algoritme vast en vertrouw nooit de alg uit de header. Gebruik sterke, geroteerde sleutels die je in een KMS bewaart. Valideer exp, nbf, iat, iss en aud bij elke aanvraag. Sla tokens op waar XSS niet bij kan.
Om het in de praktijk te brengen: plak een willekeurig token in onze JWT-decoder om het algoritme en de claims te inspecteren en risico’s als alg:none of HS-verwarring op te sporen, en gebruik de JWT-encoder om volledig in je browser met ondertekenen te experimenteren. Je sleutels verlaten je apparaat nooit.