JWT-autentisering i praktiken: Signering, lagring och fällorna som läcker sessioner
"Stop using JWT for sessions" är fortfarande omdebatterat tio år senare. HS256 vs RS256, varför algoritm-låsning är kritiskt, och varför localStorage är fel ställe att spara tokens på.
"Stop using JWT for sessions" har varit rubriken på ett av de mest delade säkerhetsinläggen i utvecklarcommunityn sedan 2016 — och ändå är JSON Web Tokens idag standardvalet i nästan varje ny API som byggs. Motsägelsen är inte så konstig som den låter: JWT löser ett specifikt problem riktigt bra, och skapar nya problem när det används för fel sak. Den här guiden går igenom hur JWT faktiskt fungerar, var det passar, och de misstag som gör att autentiseringen din app bygger på plötsligt blir en säkerhetslucka.
Om du bara vill ha grunderna för hur ett REST-API sätts upp överhuvudtaget, kolla gärna vår guide till modern webbutveckling först. Den här artikeln förutsätter att du redan bygger eller underhåller ett API.
Vad ett JWT faktiskt är
Ett JWT är tre base64url-kodade delar separerade med punkter: header.payload.signature. Header beskriver algoritm och typ, payload innehåller dina claims (användar-id, roller, utgångstid), och signaturen bevisar att innehållet inte manipulerats — inte att det är hemligt.
// Header
{ "alg": "HS256", "typ": "JWT" }
// Payload — synligt för alla som har token, ALDRIG hemlig data
{ "sub": "user_42", "role": "admin", "iat": 1893456000, "exp": 1893459600 }
// Signature = HMACSHA256(base64(header) + "." + base64(payload), secret)Den vanligaste missuppfattningen: payload är bara kodad, inte krypterad. Klistra in valfritt JWT på jwt.io och du läser hela innehållet direkt. Lägg aldrig lösenord, personnummer eller annan känslig data i ett JWT.
Signera — HS256 vs RS256
Det här valet avgör hur din arkitektur kan skalas, och det är svårt att ändra i efterhand.
// HS256 — symmetrisk nyckel. Samma hemlighet signerar OCH verifierar.
// Passar: en monolit där samma tjänst gör båda delarna.
const token = jwt.sign(payload, process.env.JWT_SECRET, { algorithm: "HS256" });
// RS256 — asymmetriskt nyckelpar. Privat nyckel signerar, publik verifierar.
// Passar: mikrotjänster där flera tjänster behöver verifiera tokens
// utan att kunna utfärda nya själva.
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
// Andra tjänster verifierar med bara den publika nyckeln — de kan
// aldrig förfalska ett token även om de blir komprometterade.Kritisk fälla: validera aldrig algoritmen utifrån vad token själv påstår sig använda. Ett känt angrepp byter alg till none eller försöker tvinga fram HS256-verifiering med den publika RS256-nyckeln som hemlighet. Bibliotek som accepterar algoritmen dynamiskt öppnar dörren. Lås alltid fast vilken algoritm servern förväntar sig:
// ❌ Sårbart — litar på tokens egen algoritm
jwt.verify(token, secret);
// ✅ Låst till EN algoritm — token kan inte styra hur den verifieras
jwt.verify(token, secret, { algorithms: ["HS256"] });Varför "JWT för sessions" är omdebatterat
Grundproblemet joepie91s inlägg pekar på 2016 gäller fortfarande: ett vanligt server-side sessions-id kan spärras direkt — du raderar raden i databasen. Ett JWT är giltigt tills det går ut, oavsett vad som händer på servern under tiden. Loggar en användare ut, byter roll, eller blir avstängd, fortsätter deras gamla token att fungera tills exp passeras.
// Vanliga lösningar på "kan inte spärra JWT" — alla är kompromisser:
// 1. Korta livstider + refresh tokens
{ exp: "15m" } // access token — kort risk-fönster
{ exp: "7d" } // refresh token — lagras säkert, kan spärras i databas
// 2. Denylist för spärrade tokens (kräver ändå en databas-slagning —
// du tappar poängen med att vara "stateless")
const isRevoked = await redis.get(`revoked:${tokenId}`);
// 3. Kort livstid utan refresh — enklast, men tvingar omloggning oftaSlutsatsen är inte "använd aldrig JWT" — det är "använd JWT för det de är bra på". Korta, verifierbara access tokens mellan tjänster som inte behöver prata med en central auth-server för varje request. Långlivade sessioner med behov av att spärras direkt hör hemma i en vanlig databas-backad session istället.
Lagring på klienten: localStorage vs httpOnly cookie
Det här är där de flesta produktionsappar faktiskt läcker tokens — inte i signeringslogiken.
// ❌ localStorage — läsbart av VILKEN SOM HELST JavaScript-kod på sidan.
// En enda XSS-sårbarhet (t.ex. ett osäkrat npm-paket) läcker alla tokens.
localStorage.setItem("token", jwt);
// ✅ httpOnly cookie — osynlig för JavaScript, skyddad mot XSS.
// Kräver istället CSRF-skydd eftersom cookien skickas automatiskt.
res.cookie("token", jwt, {
httpOnly: true,
secure: true,
sameSite: "strict",
});sameSite: "strict" täcker det mesta av CSRF-ytan på egen hand i moderna webbläsare, men kombinera gärna med en CSRF-token för formulär som ändrar data om din app har äldre trafik.
Verifiera på servern — gör inte det här själv
Att implementera signaturverifiering, expiry-kontroll och algoritm-låsning för hand är ett minfält. Använd ett underhållet bibliotek och håll det uppdaterat:
// Node.js — jsonwebtoken
import jwt from "jsonwebtoken";
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ["HS256"],
issuer: "kodlexikon.se",
maxAge: "15m",
});
} catch (err) {
// TokenExpiredError, JsonWebTokenError — hantera separat,
// läck aldrig detaljer om VARFÖR verifieringen misslyckades till klienten
return res.status(401).json({ error: "Ogiltig session" });
}Checklista för produktion
- Lås algoritmen explicit vid verifiering — lita aldrig på vad token själv säger.
- Korta access tokens (15 min eller mindre) plus refresh tokens som kan spärras i en databas.
- httpOnly + secure + sameSite-cookies istället för localStorage om du kan välja.
- Lägg aldrig känslig data i payload — den är kodad, inte krypterad.
- Rotera signeringsnycklar med en
kid-header så gamla tokens fortsätter verifieras medan nya nycklar rullas ut. - Logga misslyckade verifieringar — upprepade fel från samma IP är ofta ett tecken på att någon testar manipulerade tokens.
Nordisk vinkel: varför det här är relevant just nu
BankID-integrationer, Klarna-liknande checkout-flöden och Supabase/Auth0-baserad autentisering är standard i svenska SaaS-produkter — och nästan alla av dem utfärdar JWT under huven. Skillnaden mellan en app som håller när trafiken växer och en som läcker sessioner i en pentest-rapport ligger sällan i valet av bibliotek. Den ligger i om algoritmen är låst, om livstiden är kort, och om token lagras någonstans JavaScript inte kan läsa den.
Källor och vidare läsning
- jwt.io — Introduction to JSON Web Tokens
Officiell genomgång av strukturen, med interaktiv debugger för att inspektera tokens.
jwt.io/introduction - RFC 7519 — JSON Web Token (JWT)
Standarddokumentet som definierar formatet, claims och verifieringsregler.
rfc-editor.org/rfc/rfc7519 - OWASP — JSON Web Token Cheat Sheet
Konkreta säkerhetsrekommendationer: algoritm-låsning, nyckelhantering och vanliga implementationsfel.
cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html