Sessiebeheer voor webapps: cookies vs JWTs vs refresh
Vergelijk sessiebeheer voor webapps: cookie-sessies, JWTs en refresh-tokens, met concrete dreigingsmodellen en realistische uitlog-eisen.

Wat sessiebeheer echt doet
Een sessie is hoe je app één vraag beantwoordt nadat iemand is ingelogd: "Wie ben je nu?" Zodra dat antwoord betrouwbaar is, kan de app beslissen wat de gebruiker mag zien, wat hij mag wijzigen en welke acties geblokkeerd moeten worden.
"Ingelogd blijven" is ook een veiligheidskeuze. Je bepaalt hoe lang een gebruikersidentiteit geldig blijft, waar het bewijs van die identiteit ligt en wat er gebeurt als dat bewijs wordt gekopieerd.
De meeste webapp-constructies vertrouwen op drie bouwstenen:
- Cookie-gebaseerde server-sessies: de browser slaat een cookie op en de server kijkt bij elk verzoek de sessie na.
- JWT access tokens: de client stuurt een ondertekend token dat de server kan verifiëren zonder database-lookup.
- Refresh tokens: een langer levende referentie die wordt gebruikt om nieuwe, kortdurende access tokens te krijgen.
Dit zijn niet zozeer concurrerende "stijlen" als verschillende manieren om met dezelfde afwegingen om te gaan: snelheid versus controle, eenvoud versus flexibiliteit, en "kunnen we dit nu intrekken?" versus "loopt het vanzelf af?".
Een nuttige manier om elk ontwerp te evalueren: als een aanvaller kopieert wat je app gebruikt als bewijs (een cookie of token), wat kan die aanvaller dan doen en hoe lang? Cookie-sessies winnen vaak wanneer je sterke server-side controle nodig hebt, zoals gedwongen uitloggen of directe blokkering. JWTs passen goed bij stateless verificatie tussen services, maar ze worden lastig als je onmiddellijke intrekking nodig hebt.
Er is geen universele winnaar. De juiste aanpak hangt af van je threat model, hoe strikt je uitlog-eisen zijn en hoeveel complexiteit je team realistisch kan onderhouden.
Dreigingsmodellen die het juiste antwoord veranderen
Goed sessieontwerp draait minder om "het beste" token-type en meer om welke aanvallen je daadwerkelijk moet overleven.
Als een aanvaller data uit browseropslag (zoals localStorage) steelt, zijn JWT access tokens makkelijk te grijpen omdat pagina-JavaScript ze kan lezen. Een gestolen cookie is anders: als die als HttpOnly is ingesteld, kan normale pagina-code die niet lezen, dus eenvoudige "token-grab" aanvallen worden lastiger. Maar als de aanvaller het apparaat heeft (verloren laptop, malware, gedeelde computer), kunnen cookies nog steeds uit het browserprofiel gekopieerd worden.
XSS (kwaadaardige code die in je pagina draait) verandert alles. Met XSS heeft de aanvaller misschien niets nodig om te stelen: hij kan de al ingelogde sessie van het slachtoffer gebruiken om acties uit te voeren. HttpOnly-cookies helpen voorkomen dat sessiegeheimen worden uitgelezen, maar ze stoppen een aanvaller niet om verzoeken vanaf de pagina te doen.
CSRF (een andere site die ongewilde acties triggert) bedreigt vooral cookie-gebaseerde sessies, omdat browsers cookies automatisch meesturen. Als je op cookies vertrouwt, heb je duidelijke CSRF-verdedigingen nodig: bewuste SameSite-instellingen, anti-CSRF-tokens en zorgvuldige afhandeling van staatveranderende verzoeken. JWTs die in een Authorization-header worden gestuurd zijn minder gevoelig voor klassieke CSRF, maar ze blijven kwetsbaar voor XSS als ze opgeslagen zijn waar JavaScript ze kan lezen.
Replay-aanvallen (het opnieuw gebruiken van een gestolen credential) zijn waar server-side sessies uitblinken: je kunt een sessie-ID direct ongeldig maken. Kortlevende JWTs verkleinen de hergebruiksperiode, maar ze stoppen replay niet zolang het token geldig is.
Gedeelde apparaten en verloren telefoons maken "uitloggen" tot een echt threat model. De beslissingen komen meestal neer op vragen als: kan een gebruiker uitloggen op andere apparaten, hoe snel moet dat effect hebben, wat gebeurt er als een refresh-token wordt gestolen en laat je "onthoud mij"-sessies toe? Veel teams hanteren strengere standaarden voor staf-toegang dan voor klant-toegang, wat timeouts en intrekkingsverwachtingen verandert.
Cookie-sessies: hoe ze werken en wat ze beschermen
Cookie-gebaseerde sessies zijn de klassieke opzet. Na aanmelding maakt de server een sessierecord (vaak een ID plus velden zoals gebruikers-ID, aanmaaktijd en vervaldatum). De browser slaat alleen de sessie-ID in een cookie op. Bij elk verzoek stuurt de browser die cookie terug en bepaalt de server wie de gebruiker is door de sessie op te zoeken.
Het grote beveiligingsvoordeel is controle. De sessie wordt elke keer op de server gevalideerd. Als je iemand eruit wilt gooien, verwijder of schakel je de server-side sessie uit en werkt de cookie niet meer onmiddellijk, zelfs als de gebruiker de cookie nog heeft.
Veel van de bescherming komt van cookie-instellingen:
- HttpOnly: voorkomt dat JavaScript de cookie leest.
- Secure: stuurt de cookie alleen over HTTPS.
- SameSite: beperkt wanneer de browser de cookie meestuurt bij cross-site verzoeken.
Waar je sessiestatus opslaat beïnvloedt de schaalbaarheid. Sessies in applicatiegeheugen houden is eenvoudig, maar breekt zodra je meerdere servers draait of vaak herstart. Een database werkt goed voor duurzaamheid. Redis is gebruikelijk als je snelle lookups en veel actieve sessies wilt. Het belangrijkste punt blijft: de server moet de sessie bij elk verzoek kunnen vinden en valideren.
Cookie-sessies passen goed wanneer je strikt uitloggedrag nodig hebt, zoals staff-dashboards of klantportalen waar een admin iemand direct moet kunnen uitloggen. Als een medewerker vertrekt, beëindigt het uitschakelen van hun server-side sessies direct de toegang, zonder te wachten op token-verval.
JWT access tokens: sterktes en scherpe randen
Een JWT (JSON Web Token) is een ondertekende string met een paar claims over de gebruiker (zoals gebruikers-ID, rol, tenant) plus een vervaltijd. Je API verifieert de handtekening en vervaldatum lokaal, zonder database-oproep, en autoriseert vervolgens het verzoek.
Daarom zijn JWTs populair in API-first producten, mobiele apps en systemen waar meerdere services dezelfde identiteit moeten verifiëren. Als je meerdere backend-instances hebt, kan elk instance hetzelfde token verifiëren en hetzelfde antwoord geven.
Sterktes
JWT access tokens zijn snel te controleren en eenvoudig mee te sturen met API-calls. Als je frontend veel endpoints aanroept, kan een kortlevend access token de flow eenvoudig houden: verifieer handtekening, lees gebruikers-ID, ga door.
Voorbeeld: een klantenportal roept "List invoices" en "Update profile" aan op verschillende services. Een JWT kan de klant-ID en een rol zoals customer dragen, zodat elke service het verzoek kan autoriseren zonder elke keer een sessielookup.
Scherpe randen
De grootste afweging is intrekking. Als een token een uur geldig is, is het meestal overal geldig voor dat uur, zelfs als de gebruiker op "log out" klikt of een admin het account uitschakelt, tenzij je extra server-side checks toevoegt.
JWTs lekken ook op gewone manieren. Veelvoorkomende foutpunten zijn localStorage (XSS kan het lezen), browsergeheugen (kwaadaardige extensies), logs en foutrapporten, proxies en analytics-tools die headers vastleggen, en gekopieerde tokens in supportchats of screenshots.
Daarom werken JWT access tokens het beste voor kortdurende toegang, niet voor "voor altijd ingelogd". Houd ze minimaal (geen gevoelige persoonlijke gegevens erin), houd verval kort en ga ervan uit dat een gestolen token bruikbaar is totdat het verloopt.
Refresh tokens: JWT-opstellingen werkbaar maken
JWT access tokens zijn bedoeld om kortlevend te zijn. Dat is goed voor veiligheid, maar het creëert een praktisch probleem: gebruikers zouden niet elke paar minuten opnieuw moeten inloggen. Refresh tokens lossen dat op door de app stilletjes een nieuw access token te laten ophalen wanneer het oude verloopt.
Waar je het refresh-token opslaat, is nog belangrijker dan waar je het access-token opslaat. In een browsergebaseerde webapp is de veiligste standaard een HttpOnly, Secure cookie zodat JavaScript er niet bij kan. LocalStorage is makkelijker te implementeren, maar ook makkelijker te stelen als je ooit een XSS-bug hebt. Als je threat model XSS omvat, vermijd dan het plaatsen van langlevende geheimen in voor JavaScript toegankelijke opslag.
Rotatie maakt refresh-tokens praktisch in echte systemen. In plaats van hetzelfde refresh-token wekenlang te gebruiken ruil je het elke keer in: de client presenteert refresh-token A, de server geeft een nieuw access token plus refresh-token B uit, en refresh-token A wordt ongeldig.
Een eenvoudige rotatie-opzet volgt meestal een paar regels:
- Houd access tokens kort (minuten, niet uren).
- Sla refresh tokens server-side op met status en laatst-gebruikte tijd.
- Roteer bij elk refresh en maak het vorige token ongeldig.
- Bind refresh tokens aan een apparaat of browser waar mogelijk.
- Log refresh-events zodat je misbruik kunt onderzoeken.
Detectie van hergebruik is het belangrijkste alarm. Als refresh-token A al was ingewisseld, maar je ziet het later opnieuw, ga er dan vanuit dat het is gekopieerd. Een veelgebruikte reactie is het intrekken van de hele sessie (en vaak alle sessies voor die gebruiker) en een nieuwe aanmelding vereisen, omdat je niet kunt weten welke kopie echt is.
Voor uitloggen heb je iets nodig dat de server kan afdwingen. Dat betekent meestal een session-tabel (of een revocation-lijst) die refresh tokens als ingetrokken markeert. Access tokens kunnen nog werken totdat ze verlopen, maar je kunt dat venster klein houden door access tokens kort te maken.
Logout-eisen en wat je daadwerkelijk kunt afdwingen
Uitloggen klinkt simpel totdat je het definieert. Vaak zijn er twee verschillende verzoeken: "uitloggen dit apparaat" (één browser of één telefoon) en "uitloggen overal" (alle actieve sessies op alle apparaten).
Er is ook een timingvraag. "Onmiddellijk uitloggen" betekent dat de app het bewijs nu direct niet meer accepteert. "Uitloggen bij verval" betekent dat de app stopt met accepteren wanneer de huidige sessie of token natuurlijk verloopt.
Bij cookie-gebaseerde sessies is onmiddellijk uitloggen eenvoudig omdat de server de sessie beheert. Je verwijdert de cookie op de client en maakt het server-side sessierecord ongeldig. Als iemand de cookie eerder kopieerde, is het de serverafwijzing die daadwerkelijk het uitloggen afdwingt.
Bij alleen JWT-auth (stateless access tokens zonder server-check) kun je niet echt onmiddellijke uitloggarantie geven. Een gestolen JWT blijft geldig totdat het verloopt, omdat de server nergens kan controleren "is dit token ingetrokken?" Je kunt een denylist toevoegen, maar dan bewaar je staat en moet je die check doen, wat veel van de oorspronkelijke eenvoud weghaalt.
Een praktische patroon is access tokens kortlevend te behandelen en uitloggen via refresh tokens af te dwingen. Het access token mag een paar minuten meedraaien, maar het refresh token is wat een sessie in stand houdt. Als een laptop wordt gestolen, snijdt het intrekken van de refresh-tokenfamilie toekomstige toegang snel af.
Wat je realistisch aan gebruikers kunt beloven:
- Uitloggen dit apparaat: intrek die sessie of dat refresh-token en verwijder lokale cookies of opslag.
- Uitloggen overal: intrek alle sessies of alle refresh-tokenfamilies voor het account.
- "Onmiddellijk" effect: gegarandeerd bij server-sessies, best-effort bij access tokens totdat ze verlopen.
- Geforceerde uitlogevents: wachtwoordwijziging, account uitschakelen, rolverlagingen.
Bij wachtwoordwijzigingen en accountuitschakeling, vertrouw niet op "de gebruiker zal uitloggen." Sla een account-brede sessieversie op (of een "token valid after"-timestamp). Vergelijk deze bij elk refresh (en soms bij elk verzoek). Als het veranderd is, weiger dan en eis opnieuw aanmelden.
Stap-voor-stap: kies een sessiebenadering voor je app
Als je wilt dat sessieontwerp simpel blijft, bepaal eerst je regels en kies pas daarna de mechaniek. De meeste problemen beginnen wanneer teams JWTs of cookies kiezen omdat ze populair zijn, niet omdat ze bij de risico's en uitlog-eisen passen.
Begin met het opsommen van elke plek waar een gebruiker inlogt. Een webapp gedraagt zich anders dan een native mobiele app, een intern admin-gereedschap of een partnerintegratie. Elk verandert wat veilig kan worden opgeslagen, hoe aanmeldingen vernieuwd worden en wat "uitloggen" moet betekenen.
Een praktische volgorde die voor de meeste teams werkt:
- Maak een lijst van je clients: web, iOS/Android, interne tools, third-party toegang.
- Kies een standaard threat model: XSS, CSRF, gestolen apparaat.
- Bepaal wat uitloggen moet garanderen: dit apparaat, alle apparaten, admin-gedwongen uitloggen.
- Kies een basismodel: cookie-gebaseerde sessies (server onthoudt) of access token + refresh token.
- Stel timeouts en reactieregels in: idle versus absolute verval, plus wat je doet bij verdacht hergebruik.
Documenteer vervolgens de exacte beloften van je systeem. Voorbeeld: "Websessies verlopen na 30 minuten inactiviteit of 7 dagen absoluut. Admin kan binnen 60 seconden geforceerd uitloggen. Verloren telefoon kan op afstand worden uitgeschakeld." Die zinnen zijn belangrijker dan de bibliotheek die je gebruikt.
Zet ten slotte monitoring op die bij je patroon past. Voor token-opstellingen is een sterk signaal refresh-tokenhergebruik (hetzelfde refresh-token dat twee keer wordt gebruikt). Behandel het als waarschijnlijk diefstal, intrek de sessiefamilie en waarschuw de gebruiker.
Veelvoorkomende fouten die tot accountovername leiden
De meeste accountovernames zijn geen "slimme hacks." Het zijn simpele overwinningen door voorspelbare sessiefouten. Goed sessiebeheer draait vooral om het aanvallers niet makkelijk maken om referenties te stelen of te hergebruiken.
Een veelvoorkomende val is access tokens in localStorage plaatsen en hopen dat je nooit XSS krijgt. Als er enige script draait op je pagina (een slechte dependency, een geïnjecteerd widget, een opgeslagen comment), kan het localStorage lezen en het token naar buiten sturen. Cookies met de HttpOnly-vlag verminderen dat risico omdat JavaScript ze niet kan lezen.
Een andere val is JWTs langlevend maken om refresh tokens te vermijden. Een access token van 7 dagen is een hergebruikvenster van 7 dagen als het lekt. Een kort access token plus goed beheerd refresh token is lastiger te misbruiken, vooral als je refresh kunt afsnijden.
Cookies hebben hun eigen valkuil: CSRF-verdediging vergeten. Als je app cookie-sessies gebruikt en staatveranderende verzoeken accepteert zonder CSRF-bescherming, kan een kwaadaardige site een ingelogde browser misleiden om geldige verzoeken te sturen.
Andere fouten die vaak uit incidentreviews naar voren komen:
- Refresh tokens roteren nooit, of ze roteren maar je detecteert hergebruik niet.
- Je ondersteunt meerdere aanmeldmethoden (cookie sessie en bearer token) maar de serverregel "welke wint" is onduidelijk.
- Tokens belanden in logs (browserconsole, analytics events, server request logs), waar ze gekopieerd en bewaard worden.
Een concreet voorbeeld: een supportmedewerker plakt een "debug log" in een ticket. De log bevat een Authorization-header. Iedereen met toegang tot het ticket kan dat token replayen en handelen als de medewerker. Behandel tokens als wachtwoorden: print ze niet, sla ze niet op en houd ze kortlevend.
Snelle controles voor je live gaat
De meeste sessiebugs gaan niet over fancy cryptografie. Het zijn één ontbrekende vlag, één token dat te lang leeft of één endpoint dat opnieuw authenticatie had moeten vereisen.
Voordat je releaset, doe een korte controle gericht op wat een aanvaller kan doen met een gestolen cookie of token. Het is een van de snelste manieren om de beveiliging te verbeteren zonder je hele auth-setup te herschrijven.
Checklijst vóór release
Loop deze controles door in staging en daarna nogmaals in productie-achtige omstandigheden:
- Houd access tokens kortlevend (minuten) en bevestig dat de API ze daadwerkelijk afwijst na verval.
- Behandel refresh tokens als wachtwoorden: bewaar ze waar JavaScript ze niet kan lezen als dat mogelijk is, stuur ze alleen naar het refresh-endpoint en roteer ze na elk gebruik.
- Als je cookies voor auth gebruikt, verifieer flags: HttpOnly aan, Secure aan en SameSite bewust ingesteld. Controleer ook dat cookie-scope (domain en path) niet breder is dan nodig.
- Als cookies verzoeken authenticeren, voeg CSRF-verdediging toe en bevestig dat staatveranderende endpoints zonder CSRF-signaal falen.
- Maak intrekking echt: na wachtwoord-reset of accountuitschakeling moeten bestaande sessies snel stoppen (server-side sessieverwijdering, refresh-tokenintrekking of een "session version"-check).
Test daarna je uitlogbeloften. "Uitloggen" betekent vaak "lokale sessie verwijderen," maar gebruikers verwachten meer.
Een praktische test: meld aan op laptop en telefoon, wijzig vervolgens het wachtwoord. De laptop moet worden uitgelogd bij het volgende verzoek, niet uren later. Als je "uitloggen overal" en een apparaatoverzicht aanbiedt, bevestig dat elk apparaat correspondeert met een aparte sessie of refresh-tokenrecord die je kunt intrekken.
Voorbeeld: een klantenportal met staf-accounts en geforceerd uitloggen
Stel je een klein bedrijf voor met een webklantenportal (klanten bekijken facturen, openen tickets) en een mobiele app voor veldmedewerkers (klussen, notities, foto’s). Medewerkers werken soms offline in kelders, dus de app moet even zonder verbinding blijven werken. Admins willen ook een grote rode knop: als een tablet kwijt is of een aannemer vertrekt, kunnen ze iemand geforceerd uitloggen.
Voeg drie veelvoorkomende dreigingen toe: gedeelde tablets in busjes (iemand vergeet uit te loggen), phishing (een medewerker typt inloggegevens op een nep-pagina) en af en toe een XSS-bug in het portal (een script in de browser probeert alles te stelen wat het kan).
Een praktische opzet is hier kortlevende access tokens plus roterende refresh tokens, met server-side intrekking. Dat geeft snelle API-calls en offline-tolerantie, terwijl admins toch sessies kunnen afsluiten.
Dit zou er zo uit kunnen zien:
- Access token-levensduur: 5 tot 15 minuten.
- Refresh-tokenrotatie: bij elke refresh komt een nieuw refresh-token en wordt het oude ongeldig.
- Sla refresh tokens veilig op: op web, bewaar het refresh-token in een HttpOnly, Secure cookie; op mobiel, bewaar het in de beveiligde opslag van het OS.
- Houd refresh tokens server-side bij: sla een tokenrecord op (gebruiker, apparaat, uitgegeven tijd, laatst gebruikt, ingetrokken vlag). Als een geroteerd token opnieuw wordt gebruikt, behandel dat als diefstal en trek de hele keten in.
Geforceerd uitloggen wordt afdwingbaar: de admin trekt het refresh-tokenrecord voor dat apparaat (of alle apparaten voor die gebruiker) in. Het gestolen apparaat kan het huidige access token blijven gebruiken totdat het verloopt, maar het kan geen nieuw token krijgen. De maximale tijd om toegang volledig te kappen is dus de levensduur van je access token.
Bij een verloren apparaat, definieer de regel in duidelijke taal: "Binnen 10 minuten stopt de app met synchroniseren en is opnieuw aanmelden vereist." Offline werk kan op het apparaat blijven, maar de eerstvolgende online sync moet falen totdat de gebruiker opnieuw is aangemeld.
Volgende stappen: implementeren, testen en onderhoudbaar houden
Schrijf op wat "uitloggen" in gewone producttaal betekent. Bijvoorbeeld: "Uitloggen verwijdert toegang op dit apparaat," "Uitloggen overal gooit alle apparaten binnen 1 minuut eruit," of "Wachtwoordwijziging logt andere sessies uit." Die beloften bepalen of je server-side sessiestatus, intrekkingslijsten of kortlevende tokens nodig hebt.
Zet de beloften om in een klein testplan. Token- en sessiebugs lijken vaak goed in happy-path demo’s en falen later in het echte leven (slaapstand, slecht netwerk, meerdere apparaten).
Praktische test-checklist
Voer tests uit die de rommelige gevallen dekken:
- Verval: toegang stopt wanneer het access token of de sessie verloopt, zelfs als de browser openblijft.
- Intrekking: na "uitloggen overal" faalt de oude referentie bij het volgende verzoek.
- Rotatie: refresh-tokenrotatie geeft een nieuw refresh-token en maakt het oude ongeldig.
- Hergebruiksdetectie: het opnieuw afspelen van een oud refresh-token veroorzaakt een lock-down reactie.
- Multi-device: regels voor "alleen dit apparaat" versus "alle apparaten" worden afgedwongen en de UI komt overeen met het gedrag.
Na tests, doe een eenvoudige aanvalsoefening met je team. Kies drie scenario’s en loop ze end-to-end door: een XSS-bug die tokens kan lezen, een CSRF-poging tegen cookie-sessies en een gestolen telefoon met een actieve sessie. Je controleert of je ontwerp overeenkomt met je beloften.
Als je snel moet leveren, beperk custom glue-code. AppMaster (appmaster.io) is één optie wanneer je een gegenereerde, productieklare backend plus web- en native mobiele apps wilt, zodat je regels zoals verval, rotatie en geforceerd uitloggen consistent over clients kunt houden.
Plan een follow-up review na de lancering. Gebruik echte supporttickets en incidenten om timeouts, sessielimieten en "uitloggen overal"-gedrag aan te passen, en loop daarna dezelfde checklist opnieuw zodat fixes niet stilletjes terugzakken.


