Den dag mit første login blev hacket i teorien
At vælge login-løsning er lidt som at vælge lås til hoveddøren. Du kan købe den dyreste sikkerhedsdør, men hvis du altid lader nøglen sidde i låsen, hjælper det ikke meget. Med login er “låsen” typisk sessions eller JWT, og “nøglen” er, hvordan du gemmer tokens og cookies.
Jeg har selv været der: Første gang jeg skulle bygge login til en lille side til en ven, googlede jeg “moderne authentication” og endte selvfølgelig i en skov af artikler om JWT. Så jeg brugte JWT. Ikke fordi jeg forstod forskellen på jwt vs session, men fordi “det gør man”. Det viste sig at være en halvdårlig grund.
I den her artikel går jeg igennem, hvordan sessions og JWT faktisk virker, hvornår du bør vælge hvad, og hvor du meget nemt får lavet et sikkerhedshul, uden at opdage det.
Problemet vi faktisk prøver at løse: holde en bruger logget ind
Lad os starte simpelt: Du har en bruger, der skriver brugernavn og kodeord. Du tjekker dem, og de er rigtige. Hvad nu?
Browseren skal kunne sende noget med i efterfølgende requests, så serveren ved: “Det her er stadig Jonas, han er logget ind”.
Groft sagt har du to hovedmuligheder:
- Sessions med cookies – serveren husker tilstanden, du gemmer kun et id i browseren.
- JWT (JSON Web Tokens) – alle oplysninger ligger i selve token, og serveren kan verificere det uden at slå op i en database.
Begge dele bruger ofte cookies. Forskellen er, om serveren har en “session-tabel” eller ej.
Sessions i praksis: den klassiske løsning
Sessions er som en garderobebillet i teateret. Du får en lille seddel (session-id), og selve jakken hænger inde bagved (server-side state).
Hvordan en session-baseret login-flow typisk ser ud
Standard-flowet:
- Bruger sender
POST /loginmed brugernavn + kodeord. - Serveren validerer, og hvis det er ok, opretter den en session i hukommelse/DB:
// Pseudo i Node/Express-stil
const sessionId = randomSecureId();
sessionStore.set(sessionId, {
userId: user.id,
roles: ["user"],
createdAt: Date.now()
});
// Svar til browser
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
- Browseren gemmer cookie automatisk.
- Ved næste request sender browseren:
GET /profil
Cookie: sessionId=abc123
Serveren laver et opslag i sessionStore og ser: “Ah, det er userId 42”.
Fordele ved sessions
- Nem logout: Du sletter bare sessionen i serverens sessionStore.
- Nem at forstå for begyndere. Der er en række tutorials på fx Coding Class, der bruger den model.
- Mindsker eksponeret data i browseren. Der ligger kun et tilfældigt id i cookien.
Ulemper ved sessions
- Serveren skal gemme state – det kan være tungt, hvis du har millioner af brugere.
- Skaleringsbesvær: Hvis du kører flere servere, skal de dele sessionStore (Redis, database osv.).
- API til mobil/3. parts-klienter kan være lidt mere bøvlede at få til at spille sammen, hvis alt er tænkt til klassisk web.
JWT i praksis: oplysningerne ligger i selve token
JWT (JSON Web Token) er som et plastikkort med alle oplysninger printet på og en underskrift, som alle kan tjekke. Serveren behøver ikke kigge i et kartotek; den kan se på kortet, om det er gyldigt.
Hvad er en JWT?
En JWT består af tre dele, adskilt af punktummer:
xxxxx.yyyyy.zzzzz
- Header – typisk algoritme og type.
- Payload – data som
userId,roles,exp(udløbstid). - Signature – kryptografisk signatur baseret på en hemmelig nøgle.
Payload er bare Base64-enkodet JSON. Ikke krypteret. Så alle kan læse det, hvis de får fat i token. De kan bare ikke ændre det uden at ødelægge signaturen.
JWT login-flow i praksis
- Bruger sender
POST /loginmed brugernavn + kodeord. - Serveren laver en JWT:
const jwt = signJWT({
sub: user.id, // subject
role: "user",
exp: now + 15 * 60 // 15 minutter
}, SECRET_KEY);
// Variant A: sende direkte i respons-body
{
"accessToken": "xxxxx.yyyyy.zzzzz"
}
// Variant B: sende i cookie
Set-Cookie: accessToken=xxxxx.yyyyy.zzzzz; HttpOnly; Secure; SameSite=Lax
- Ved næste request sender klienten JWT.
GET /profil
Authorization: Bearer xxxxx.yyyyy.zzzzz
Eller automatisk via cookie, hvis du går den vej.
Fordele ved JWT
- Stateless: Serveren skal ikke holde styr på sessioner.
- God til APIs, især når flere klienter skal bruge den samme backend (SPA, mobil-app, tredjeparts integration).
- Indbygget udløb (
exp) og claims, der kan bruges direkte i autorisation (roller osv.).
Ulemper ved JWT
- Logout er sværere, fordi token teknisk set er gyldig indtil
exp. - Store tokens – hvis du putter for meget info i payload.
- Flere måder at skyde sig selv i foden på, især med opbevaring (localStorage), for lange levetider og manglende rotation.
JWT vs session – før og nu
Her er den overordnede sammenligning mellem server-side sessions og JWT:
| Feature | Sessions (server-side) | JWT (stateless) |
|---|---|---|
| Hvor gemmes state? | På server (sessionStore) | I token (klienten) |
| Logout | Nem – slet session | Sværere – kræver blacklist eller korte tokens |
| Skalérbarhed | Kræver delt sessionStore | Ingen server-state, lettere at skalere |
| Implementering | Nem i klassisk web-app | Lidt mere kompleks med refresh tokens osv. |
| Egnet til SPA/mobil | Kan lade sig gøre, men ikke altid oplagt | Ja, ofte det naturlige valg |
Jeg ser mange vælge JWT til helt simple server-renderede sider, hvor sessions ville være både nemmere og sikrere. Omvendt ser jeg også folk holde stædigt fast i sessions, når de reelt bygger et API til tre forskellige klienttyper.
Fem spørgsmål der afgør, om du skal bruge JWT eller sessions
Her er den beslutningsmodel, jeg selv ville ønske, jeg havde for 10 år siden. Tænk din app igennem med de her fem spørgsmål.
1. Er din app primært server-renderet eller en SPA?
- Server-renderet (fx klassisk Django, Laravel, Rails, Express med templating):
- Bruger klikker på links, får hele HTML-sider fra serveren.
- JavaScript bruges mest til små interaktioner.
- SPA (Single Page Application – React, Vue, Svelte osv.):
- Frontend håndterer routing, henter data via JSON-API.
- Backend er ren API, ofte uden server-renderede sider.
Min tommelfingerregel:
- Server-renderet web-app: start med sessions.
- SPA/mobil/API-first: overvej JWT (eller stadig sessions via cookies, hvis det kun er samme domæne).
2. Har du brug for central logout eller blacklist?
Med sessions kan du til enhver tid sige: “Alle sessions for bruger 42 er nu ugyldige”. Du rydder bare sessioner i serverens sessionStore.
Med JWT skal du enten:
- Bruge korte access tokens (5-15 minutter) og et refresh token-system.
- Have en blacklist over tilbagekaldte tokens i en database (og så er du tilbage med server-state).
Hvis du har krav om, at logout skal være effektivt med det samme (bank, sundhed, intern admin), er sessions ofte nemmere at styre korrekt.
3. Kører du på flere domæner eller subdomæner?
Cookies har et Domain-felt. En cookie sat på app.mitdomæne.dk deles ikke automatisk med admin.mitdomæne.dk, medmindre du sætter Domain=.mitdomæne.dk.
Hvis du har:
- En SPA på
app.example.com - Et API på
api.example.com
kan du stadig fint bruge httponly cookies til enten session-id eller JWT. Du skal bare konfigurere CORS og cookie-domain ordentligt.
Hvis du derimod har helt forskellige domæner eller tredjeparts-integrationer, ender du ofte med at sende tokens i Authorization-header. Her passer JWT ret godt ind.
4. Hvad er din trusselsmodel? (XSS vs CSRF)
To klassikere fra OWASP:
- XSS (Cross-Site Scripting): Angriber får lov at køre JavaScript i din side.
- CSRF (Cross-Site Request Forgery): Angriber får brugerens browser til at lave requests med brugerens cookies, uden at brugeren opdager det.
JWT vs session løser ingen af dem automatisk. Det er måden, du opbevarer og sender dine tokens/cookies på, der er afgørende.
Meget kort:
- Tokens i localStorage er sårbare overfor XSS. Hvis en angriber kan køre JavaScript, kan de læse din localStorage.
- Cookies er sårbare overfor CSRF, fordi browseren sender dem automatisk. Men det kan dæmpes med
SameSiteog CSRF-tokens.
Hvis du er ny i alt det her, og du spørger mig:
- Jeg vil næsten altid hellere kæmpe med CSRF (og bruge
SameSite=Lax/Strict) end at lægge tokens i localStorage og håbe på, at jeg aldrig får XSS.
5. Hvor mange klienttyper skal bruge din auth?
Hvis du kun har:
- En web-side på
www.minside.dk,
er sessions med cookies både fint og nemt.
Hvis du har:
- SPA på
app.minside.dk - Mobil-app
- Måske en CLI eller integration til andre systemer
giver JWT ofte bedre mening, fordi:
- Mobil og CLI nemt kan sende
Authorization: Bearer <token>. - Du kan have samme auth-flow på tværs af klienter.
Sikker opbevaring: httpOnly cookies vs localStorage
Her kommer den del, som ofte bliver overset, når nogen spørger “jwt vs session”. Det handler mest om hvordan du gemmer din token/session-id.
httpOnly cookies
En cookie med flaget HttpOnly kan ikke læses af JavaScript via document.cookie. Browseren sender den stadig automatisk med til serveren.
Typisk opsætning:
Set-Cookie: accessToken=xxxxx; HttpOnly; Secure; SameSite=Lax; Path=/
Fordele:
- Beskyttet mod XSS-tyveri (JS kan ikke læse værdien).
- Nemt at bruge i klassiske web-apps.
Ulemper:
- Sårbar for CSRF-angreb, medmindre du bruger
SameSiteog evt. ekstra CSRF-token. - Lidt mere bøvl i en SPA, fordi du skal få CORS, cookies og fetch/axios til at spille sammen.
localStorage (og sessionStorage)
Mange tutorials viser:
// PSEUDO - klassisk anti-pattern
localStorage.setItem("accessToken", jwt);
// Ved request
const token = localStorage.getItem("accessToken");
fetch("/api/data", {
headers: {
Authorization: `Bearer ${token}`
}
});
Det føles lækkert simpelt, især i en SPA. Men hvis en angriber kan køre JavaScript i din side (XSS), kan de bare:
const stolen = localStorage.getItem("accessToken");
// send til angribers server
Det er derfor OWASP og de fleste security-folk kraftigt fraråder at opbevare følsomme tokens i localStorage.
Mit personlige valg i dag:
- Brug httpOnly cookies til både session-id og access tokens.
- Accepter, at du skal tage CSRF seriøst.
Typiske fejl og hvordan du undgår dem
Her er nogle klassikere, jeg selv har lavet, eller jeg har set andre lave i små hobbyprojekter.
Fejl 1: Langlivede JWT uden rotation
“Vi sætter bare exp til 30 dage, så brugeren er logget ind for evigt”.
Hvis den token bliver lækket, har angriberen 30 dage at hygge sig i.
Bedre mønster:
- Korte access tokens (5-15 minutter).
- Længerelevende refresh tokens (dage/uger) gemt i httpOnly cookie.
- Mulighed for at tilbagekalde refresh tokens server-side (fx gemme dem i DB med id).
Fejl 2: JWT i localStorage + ingen XSS-sikring
Det føles nemt, men kombinationen af:
- Masser af klientside-JS
- LocalStorage tokens
- Ingen stram Content Security Policy (CSP)
er en invitation til token-tyveri, hvis du får et XSS-hul.
Bedre: Brug httpOnly cookies, sanitisér input, brug frameworks korrekt, og kig på OWASP om XSS.
Fejl 3: JWT uden signaturvalidering
Jeg har set kode som:
// ANTI-PATTERN
const payload = JSON.parse(atob(token.split(".")[1]));
// Tjekker aldrig signaturen
Så har du i praksis bare en base64-enkodet JSON, som brugeren selv kan lave. Hele pointen med JWT forsvinder.
Rigtigt mønster:
// Pseudo - brug altid et gennemtestet bibliotek
try {
const payload = verifyJWT(token, SECRET_KEY);
// verifyJWT tjekker signatur og exp
} catch (err) {
// token er ugyldig eller udløbet
}
Fejl 4: Session-cookies uden Secure/SameSite
Særligt på hobbyprojekter ser jeg cookies som:
Set-Cookie: sessionId=abc123
Ingen Secure, ingen SameSite, ingen HttpOnly.
Bedre standard i dag:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
Hvis du har en ren API til en SPA på et andet domæne, skal du selvfølgelig tune SameSite og CORS, men udgangspunktet bør være så stramt som muligt.
Mini-opsætninger: sådan kan du gøre i praksis
Nu til det sjove. Her er tre små “logbogs-setup”-eksempler fra ting, jeg selv har bygget.
Setup 1: Klassisk server-renderet app med sessions
Use case: Lille intern adminside til en forening. Kun få brugere, kun web.
Flow
POST /login– tjek brugernavn/kodeord, opret session, sæt httpOnly cookie.GET /dashboard– læssessionIdfra cookie, slå op i sessionStore, render HTML.POST /logout– slet session fra sessionStore, sæt cookie til at udløbe.
Pseudo-kode (Node/Express-stil)
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = await findUser(username);
if (!user || !(await checkPassword(password, user.hash))) {
return res.status(401).send("Forkert login");
}
const sessionId = randomSecureId();
sessionStore.set(sessionId, { userId: user.id });
res.cookie("sessionId", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax"
});
res.redirect("/dashboard");
});
function requireAuth(req, res, next) {
const sessionId = req.cookies.sessionId;
const session = sessionStore.get(sessionId);
if (!session) return res.redirect("/login");
req.userId = session.userId;
next();
}
app.get("/dashboard", requireAuth, (req, res) => {
// req.userId er sat
res.render("dashboard");
});
Det her er stadig min favorit til små klassiske sider. Simpelt og let at reasonere om.
Setup 2: SPA + API med JWT i httpOnly cookies
Use case: React-frontend på app.example.com, API på api.example.com.
Flow
- Bruger logger ind med formular i SPA.
- SPA kalder
POST https://api.example.com/loginmed credentials. - API svarer med:
- Access token i httpOnly cookie (kort levetid).
- Refresh token i httpOnly cookie (længere levetid).
- SPA kalder efterfølgende API-endpoints med
credentials: "include", så cookies sendes. - Når access token udløber, kalder SPA automatisk
/refresh, som udsteder et nyt access token, hvis refresh token stadig er gyldigt.
Pseudo-kode: login og refresh
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = await findUser(email);
if (!user || !(await checkPassword(password, user.hash))) {
return res.status(401).json({ error: "Invalid" });
}
const accessToken = signJWT({ sub: user.id }, ACCESS_SECRET, { expiresIn: "15m" });
const refreshToken = signJWT({ sub: user.id }, REFRESH_SECRET, { expiresIn: "7d" });
// Gem evt. refreshToken-id i DB hvis du vil kunne revokere
res
.cookie("accessToken", accessToken, {
httpOnly: true,
secure: true,
sameSite: "strict"
})
.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict"
})
.json({ success: true });
});
app.post("/refresh", (req, res) => {
const token = req.cookies.refreshToken;
if (!token) return res.status(401).json({ error: "No token" });
try {
const payload = verifyJWT(token, REFRESH_SECRET);
const newAccessToken = signJWT({ sub: payload.sub }, ACCESS_SECRET, { expiresIn: "15m" });
res
.cookie("accessToken", newAccessToken, {
httpOnly: true,
secure: true,
sameSite: "strict"
})
.json({ success: true });
} catch (e) {
return res.status(401).json({ error: "Invalid refresh" });
}
});
På frontend-siden sørger du for at kalde fetch med credentials:
// I din React/Vue app
fetch("https://api.example.com/data", {
method: "GET",
credentials: "include" // sender cookies med
});
Hvis du vil nørde videre i den retning, er det også værd at kigge på artikler om CORS og cookies, fx når du bevæger dig videre til mere avancerede setups på Coding Class.
Setup 3: Ren API til både web og mobil med Bearer tokens
Use case: Du har et public API, som både en mobil-app, en SPA og måske andre systemer bruger.
Her er det ret oplagt at:
- Brugere logger ind ét sted (fx via mobil eller web).
- De får et JWT access token og evt. refresh token.
- Alle klienter sender
Authorization: Bearer <token>i deres requests.
Opbevaringen:
- På mobil: sikker storage (Keychain på iOS, Keystore på Android).
- På web: helst httpOnly cookie, med mindre du har en virkelig god grund til andet.
Arkitekturen minder meget om Setup 2, men her tænker du fra start i “API-first” og definerer klare regler for, hvordan tokens udstedes og fornyes.
JWT vs session: en lille beslutningsmatrix
Hvis jeg skulle koges ned til én tabel, ville den se sådan her:
| Scenario | Anbefaling | Begrundelse |
|---|---|---|
| Lille, server-renderet web-app (én klient) | Sessions + httpOnly cookie | Nemmest at implementere og styre logout |
| SPA + API på samme domæne/subdomæne | JWT eller sessions i httpOnly cookies | Begge virker, JWT giver fleksibilitet, sessions er simple |
| API-first, flere klienttyper (web, mobil, CLI) | JWT (access + refresh tokens) | Standardmønster, nemt at bruge på tværs af klienter |
| Høj-sikkerhed, krav om øjeblikkelig logout | Sessions eller JWT + blacklist | Du skal kunne invalidere tokens centralt |
Og ja, det betyder, at det ikke altid er “JWT er moderne, sessions er gamle”. Det afhænger af, hvad du bygger.
Hvad gør du nu?
Hvis du er i gang med dit første login-system og sidder midt i “jwt vs session”-valget, ville jeg gøre sådan her:
- Svar ærligt på: Er min app egentlig bare en normal web-side? Hvis ja, brug sessions.
- Hvis du bygger en SPA, så beslut dig for, om du vil kæmpe med cookies/CORS (sikkert) eller leve med localStorage (hurtigere, men farligere).
- Uanset hvad du vælger: slå cookie-flags op og sæt
HttpOnly,SecureogSameSitebevidst. - Læg en lille note til dig selv om at kigge på JWT-intro og OWASP-artiklerne om CSRF og XSS på et tidspunkt, hvor du har kaffe og ro.
Jeg synes stadig, sessions er undervurderede til små projekter. Og jeg synes, JWT ofte bliver brugt “fordi det er nyt”, uden at man helt får de ekstra sikkerhedsbrikker med.
Hvis du kan sidde tilbage og forklare din egen løsning med sætningen: “Når brugeren er logget ind, ligger der den her ene ting i browseren, og serveren tjekker den sådan her“, så er du allerede foran mange.
Og hvis du opdager om et år, at du ville ønske, du havde valgt den anden model, så er du i øvrigt i godt selskab. Jeg har refaktoreret mit første login-system så mange gange, at det næsten fortjener sin egen mindeside. På den positive side har jeg i det mindste lært at sætte HttpOnly rigtigt.








Send kommentar
Du skal være logget ind for at skrive en kommentar.