7 valg i din auth der afgør om du fortryder om et år
Forestil dig du endelig har fået din første webapp deployet, folk logger ind, alt føles godt. Tre måneder senere opdager du at du ikke rigtigt kan logge folk rigtigt ud, og at halvdelen af dine bugs handler om tokens der lever længere end dine brugeres tålmodighed.
Det er her forskellen mellem JWT og sessions rammer dig i nakken.
Hvad du faktisk prøver at opnå med auth (og hvad du kan ignorere lidt)
Inden vi snakker jwt vs session, er det værd at stille et kedeligt, men vigtigt spørgsmål:
- Hvad er det mindste du skal have på plads, for at login ikke bliver en sikkerhedsulykke?
For 90 % af hobbyapps, små SaaS-projekter og interne værktøjer er det her typisk målet:
- Brugeren kan logge ind, og du kan genkende dem på tværs af requests.
- Du kan logge dem ud på en måde der føles ægte (ikke bare “gem knappen”).
- Et læk af én nøgle eller ét token vælter ikke hele lortet for evigt.
- Du kan skifte password / tvinge re-login uden at genopfinde kryptografi.
Det du typisk ikke behøver, selvom internettet får det til at lyde sådan:
- Mikroservice-venlig, stateless auth til 20 services og 5 mobile apps.
- Egne implementationer af OAuth 2.0 og OpenID Connect fra bunden.
- Token-hierarkier og distribuerede revocation-lister.
Hvis du sidder med en klassisk webapp eller en simpel SPA, er dit reelle valg ofte:
- Sessions i cookies: Serveren holder styr på brugeren via et session-id.
- JWT: Token der indeholder brugerinfo, signeret, valideres uden database-hit.
Begge kan være sikre. Begge kan være farlige. De fleste problemer kommer ikke fra teknologien, men fra hvordan du bruger den.
Sessions i cookies – den kedelige standard der stadig virker
Sessions er den gamle løsning. Det betyder ikke at den er dårlig. Tværtimod. OWASP anbefaler stadig klassiske sessions som default for webapplikationer.
Hvordan sessions faktisk virker
Det enkle billede:
- Bruger logger ind med email + password.
- Serveren opretter en række i en
sessions-tabel:session_id,user_id,expires_at, måske lidt ekstra metadata. - Serveren sender en cookie tilbage, fx
session_id=abc123, med nogle vigtige flag (dem tager vi lige senere). - Browseren sender
session_idmed på hver request til dit domæne. - På hver request slår du
session_idop i din tabel og finder brugeren.
Kodeagtigt i pseudo-Python/Flask:
# Ved login
session_id = generate_secure_random_id()
store_session(session_id, user_id, expires_at)
response = make_response("OK")
response.set_cookie(
"session_id",
session_id,
httponly=True,
secure=True,
samesite="Lax",
max_age=3600,
)
return response
# Ved hver request
session_id = request.cookies.get("session_id")
if session_id:
session = load_session(session_id)
if session and not session.is_expired():
current_user = load_user(session.user_id)
Det er helt bevidst, at session_id bare er en tilfældig streng og ikke indeholder brugerinfo. Alt det ligger i databasen.
Fordele ved sessions
- Nem logout: Slet rækken i
sessions-tabellen. Bum, token er død. - Nem invalidering: Nulstil alle sessions for en bruger ved fx password-skift.
- Mindre angrebsflade: Ingen brugerdata i selve tokenet. Kun et tilfældigt id.
- Gammel, gennemprøvet model: Frameworks har det her indbygget.
Ulempen alle JWT-fans peger på:
- Serveren er stateful. Du har en tabel, du skal slå op i på hver request.
Hvis du bygger en lille til mellemstor webapp med én database, er det her typisk et ikke-problem. Et enkelt index på session_id og du er videre.
Typiske fejl med sessions
Det går ofte galt her:
- Du sætter ikke
HttpOnlypå cookien, så JavaScript kan læse den. - Du glemmer
Secure, så den sendes over HTTP i dev og ender i logs. - Du laver sessions der aldrig udløber.
Opskriften på en nogenlunde sikker session-cookie:
Set-Cookie: session_id=abc123;
HttpOnly;
Secure;
SameSite=Lax;
Path=/;
Max-Age=3600
MDN har en fin oversigt over cookie-flags og deres betydning, hvis du vil nørde detaljer.
JWT – hvornår det giver mening (og hvornår det mest er besvær)
JSON Web Tokens (JWT) blev hypet som løsningen på alt. Så fandt folk ud af, at de også kunne skabe helt nye problemer.
Hvad er et JWT i praksis?
Et JWT er et token der består af tre dele:
header.payload.signature
Header og payload er JSON der er base64-url-kodet. Signature er en kryptografisk signatur over de to første.
Et klassisk eksempel:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload (claims)
{
"sub": "user_123",
"name": "Lasse",
"iat": 1714400000,
"exp": 1714403600,
"roles": ["admin"]
}
Serveren signerer det med en hemmelig nøgle. Når der kommer en request med Authorization: Bearer <token>, kan serveren verificere signaturen og stole på payloaden uden at slå op i en database.
Det er her den klassiske salgstale kommer fra: stateless authentication.
Hvornår JWT faktisk er smart
JWT giver typisk mening, hvis:
- Du har flere services der skal stole på samme token (API-gateway, mikroservices).
- Du har et eksternt identity provider (Auth0, Cognito, Keycloak).
- Du vil undgå et database-hit på hver request, og du har meget høj trafik.
Det kan også være rart til ting som:
- Et kortlivet access token til en SPA eller mobilapp.
- Adgang til tredjeparts-API’er hvor de forventer JWT.
Hvornår JWT er overkill (eller direkte en dårlig idé)
Jeg ser ofte folk bruge JWT i de her scenarier, hvor sessions næsten altid er nemmere:
- En klassisk server-side rendered webapp (Next.js, Laravel, Django, Rails).
- En lille hobby-API og en enkelt frontend der bruger den.
- Interne tools til et lille team.
Problemerne kommer især når man kombinerer:
- Langlivede JWTs (fx 7 dage eller mere).
- Ingen central revocation (du kan ikke nemt invaliderer ét token).
- Usikker storage (localStorage, sessionStorage i browseren).
OWASP har en dedikeret JWT cheat sheet, som i grove træk siger: Hold tokens kortelevende, signér dem ordentligt, og tænk over revocation.
6 spørgsmål der afgør om du skal bruge sessions eller JWT
I stedet for at starte en religionskrig om session cookie vs jwt, kan du stille dig selv de her spørgsmål.
1. Er din app primært en klassisk webapp?
Server-renderede sider, lidt AJAX, ingen kæmpe public API?
- Vælg: Session-cookies.
- Begrundelse: Frameworks har det indbygget, besværet ved JWT giver sjældent værdi.
2. Har du én frontend (SPA) og ét backend-API på samme domæne?
Fx React front på app.example.com og API på api.example.com.
- Standardvalg: Stadig sessions, men sæt domæne på cookien så den virker for begge subdomæner.
- Alternativ: Kortlivet JWT i en
HttpOnly-cookie, men så bygger du i praksis en session oven på JWT alligevel.
3. Skal du understøtte native mobile apps?
Her begynder JWT at give mere mening.
- Mobilapps kan ikke bruge browser-cookies på samme måde.
- Et access-token + refresh-token-flow er ofte mere naturligt.
Hvis mobile er vigtigt fra dag 1, og du har ét eller flere API’er, så:
- Vælg: JWT-baseret auth med kortlivede access tokens og længerelevende refresh tokens.
4. Har du flere backend-services der skal stole på samme login?
Hvis du planlægger mikroservices eller har flere uafhængige API’er, er stateless auth mere tillokkende.
- Vælg: Enten central session-service (stadig sessions) eller et fælles JWT-setup.
Hvis du er helt ny i det her, vil jeg ærligt anbefale at starte uden mikroservices. Monolit + sessions er undervurderet.
5. Har du reelt brug for at undgå database-hit på hver request?
Hvis du har 50 requests i sekundet og en simpel session-tabel med index, er det ikke her din performance flaskehals ligger.
- Hvis nej: Sessions er stadig et godt valg.
- Hvis ja: Overvej JWT eller caching, men mål først.
6. Hvem skal vedligeholde auth-koden om 2 år?
Hvis svaret er “fremtidige dig på en søndag aften”, så gå med den mest kedelige løsning der stadig er sikker nok.
- “Kedelig og sikker nok” = sessions med gode cookie-flags og fornuftig TTL.
Sikker baseline for sessions – cookies der ikke lækker ved første fejl
Hvis vi siger: “Vi vælger sessions”, hvad er så en rimelig sikker baseline?
Cookie-flags du næsten altid vil sætte
- HttpOnly: JavaScript kan ikke læse cookien. Beskytter mod at XSS bare tømmer din auth.
- Secure: Kun over HTTPS. Ingen undskyldning i 2026.
- SameSite: Mindsker CSRF-angreb. Typisk
LaxellerStrict. - Path: Ofte bare
/, men vær bevidst hvis du har flere apps. - Max-Age eller Expires: Sessions skal udløbe.
SameSite, kort forklaret
- SameSite=Lax: Cookie sendes ikke ved cross-site POST, men sendes ved navigationer og GET-links. God default.
- SameSite=Strict: Cookie sendes kun, hvis brugeren er på dit domæne. Mere sikkert, kan give problemer hvis du har loginsider der skal linkes til udefra.
- SameSite=None: Cookie sendes også på cross-site requests, men så skal
Securevære sat. Bruges tit til 3. parts cookies, men kræver ordentlig CSRF-beskyttelse.
For en klassisk webapp er Lax et fint kompromis.
Session-længde og idle-timeout
Du vil typisk have to tidsaspekter:
- Absolut udløb: Fx 24 timer efter login.
- Idle-timeout: Fx log ud efter 30 min inaktivitet.
I databasen kan du have felter som created_at og last_seen_at. Ved hver request opdaterer du last_seen_at og tjekker begge regler.
SELECT * FROM sessions
WHERE id = :id
AND created_at > now() - interval '24 hours'
AND last_seen_at > now() - interval '30 minutes';
Hvis du har brug for “forbliv logget ind”, kan du bruge to cookies: én kortlivet session-cookie og én længere refresh-cookie, lidt ligesom token-setup’et herunder.
Sikker baseline for JWT – hvis du virkelig vælger tokens
Hvis du har besluttet at JWT er relevant (fx SPA + mobil + API), så lad os gøre det uden de klassiske fælder.
Access token vs refresh token
Strukturen mange bruger:
- Access token
- Kortlivede (5-15 min).
- Bruges til at kalde API’et.
- Bæres typisk som
Authorization: Bearer <token>.
- Refresh token
- Længerelevende (dage/uger).
- Bruges kun til at hente et nyt access token.
- Opbevares mere beskyttet, fx i en
HttpOnly-cookie.
Det vigtige: Access token må godt være “billig” at invalidere, fordi den udløber hurtigt. Refresh token skal behandles som en password-lignende hemmelighed.
Hvor skal man gemme JWT?
Det her spørgsmål dukker op konstant: hvor skal man gemme jwt?
Muligheder i browseren:
- localStorage: Nem at tilgå med JavaScript. Også nem at stjæle ved XSS. Ikke godt til auth-tokens alene.
- sessionStorage: Samme problem som localStorage, bare kortere levetid.
- JavaScript memory (variabel i din app state): Bedre end storage, men tokens forsvinder ved refresh. Kræver godt refresh-flow.
- HttpOnly cookie: Kan ikke læses af JS. Beskytter mod XSS-tyveri, men så skal du tage CSRF alvorligt.
En forholdsvis sikker model for en SPA er:
- Gem refresh token i en
HttpOnly,Secure,SameSite=Lax-cookie. - Gem access token kun i JavaScript-memory (ikke localStorage).
- Hent nyt access token fra en
/refresh-endpoint der bruger refresh-cookien.
Det betyder:
- XSS-angreb kan ikke bare læse refresh token direkte fra storage.
- Du har stadig risiko for CSRF på
/refresh, så du skal tænke CSRF-mitigation ind.
Refresh token best practice (uden at blive helt enterprise)
En simpel, rimelig sikker opskrift:
- Rotate tokens
- Hver gang et refresh token bruges, udsteder du et nyt og invalidere det gamle.
- Gem refresh tokens i databasen med et unikt id og status (aktiv/revokeret).
- Bind til device
- Gem fx en
device_ideller user-agent-hash, så du kan få et fingerpeg om mistænkelig adfærd.
- Gem fx en
- Server-side logout
- Ved logout markerer du refresh token som revokeret i databasen.
- Access token vil så dø naturligt efter kort tid.
- TTL på refresh tokens
- Selv hvis rotation fejler, må tokens ikke leve for evigt.
Eksempel på tabel:
CREATE TABLE refresh_tokens (
id uuid PRIMARY KEY,
user_id uuid NOT NULL,
created_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
revoked_at timestamptz NULL,
replaced_by uuid NULL
);
Og et refresh-flow i pseudo-kode:
// POST /refresh
const refreshToken = getCookie("refresh_token");
const tokenRecord = db.refresh_tokens.find(refreshToken.id);
if (!tokenRecord || tokenRecord.revoked_at || tokenRecord.expires_at < now()) {
return 401;
}
// Rotate
const newRecord = db.refresh_tokens.insert({ user_id: tokenRecord.user_id, ... });
db.refresh_tokens.update(tokenRecord.id, { revoked_at: now(), replaced_by: newRecord.id });
const newAccessToken = signJwt({ sub: tokenRecord.user_id }, { expiresIn: "10m" });
setCookie("refresh_token", newRecord.id, httpOnlySecureSameSiteLax);
return { accessToken: newAccessToken };
Typiske fejl i jwt vs session-valget (og hvordan du undgår dem)
Her er nogle klassikere jeg ser igen og igen.
| Problem | Hvorfor det gør ondt | Bedre løsning |
|---|---|---|
Token i localStorage |
Et XSS-angreb kan læse det direkte og bruge det fra en anden browser. | Brug HttpOnly cookies til langlivede tokens, hold access tokens i memory. |
| Langlivede JWT uden revocation | Et lækket token giver adgang i ugevis, du kan ikke trække det tilbage. | Kortlivede access tokens + refresh tokens med server-side revocation. |
| Tror JWT automatisk er “stateless” | Du ender alligevel med en tokens-tabel for at kunne logge ud og spærre tokens. | Accepter at du har brug for noget state, eller brug sessions fra start. |
| CORS/CSRF-forvirring | Man blander CORS (hvem må kalde API’et) og CSRF (hvem kan misbruge cookies). | Læs op på forskellen, fx i vores artikel om CORS-fejl uden panik. |
| Ingen rotation på refresh tokens | Ét lækket refresh token kan bruges igen og igen. | Implementer rotation og server-side blacklist/flag. |
| Custom crypto | Man “forbedrer” JWT med hjemmerullet kryptering eller mærkelige algoritmer. | Brug velkendte libs, hold dig til HS256/RS256 og læs OWASP-cheatsheets. |
Tre mini-arkitekturer – sådan kunne du bygge det i virkeligheden
Nu bliver vi konkrete. Tre typiske setups, og hvad jeg personligt ville vælge.
1. Klassisk webapp med server-renderede sider
Eksempel: Django, Laravel, Rails, Next.js med SSR.
- Frontend: HTML-renderet på serveren, almindelige formularer, lidt JS.
- Backend: En monolit med database og templating.
Mit valg: klassiske sessions med secure cookies.
Flow:
- POST
/loginmed email + password. - Server verificerer, opretter session-række, sætter session-cookie.
- Alle efterfølgende requests bruger session-cookie for auth.
- Logout sletter session-række og cookien.
Ekstra sikkerhed:
- CSRF-tokens i formularer (frameworks har det indbygget).
- Session-regeneration ved login (nyt session-id, så man ikke hijacker for-login-sessionen).
Det her er det mindst eksotiske og mest driftssikre valg for de fleste lær-at-kode-projekter.
2. SPA + API på samme domæne
Eksempel: React/Vue SPA bygget med Vite, der kalder et Node/Express-API.
- Frontend: Kører i browseren, bruger
fetch/Axios mod/api. - Backend: JSON-API, muligvis også statiske filer.
Her har du to hovedveje:
Model A: Sessions i cookies (simpel)
- Brug session-cookie præcis som i webapp-scenariet.
- Din SPA kalder bare
/api/*, og serveren bruger session-cookie til at finde brugeren.
Fordele:
- Nem logout, nem invalidering.
- Mindst ekstra-funktionalitet at bygge.
Ulemper:
- Hvis du senere vil have en mobilapp, skal du tænke auth om igen.
Model B: JWT med refresh cookie (lidt mere fremtidssikret)
- Login endpoint returnerer et access token (JSON) og sætter et refresh token i en HttpOnly-cookie.
- SPA gemmer access token i memory og bruger det i
Authorization-headeren. - Når access token udløber, kalder SPA
/auth/refresh, som bruger refresh-cookien til at udstede et nyt access token.
Her er det vigtigt at du forstår forskellen på CORS og CSRF, så du ikke ender med at åbne hele API’et for alle domæner. Brug fx Chrome DevTools som beskrevet i artiklen om CORS-fejl uden panik.
3. Mobilapp + API
Eksempel: React Native-app + Node/Express eller Django REST API.
- Frontend: Native app, ingen browser-cookies.
- Backend: JSON-API, måske også brugt af en web-frontend.
Mit valg: JWT-baseret auth med refresh tokens.
Flow:
- POST
/auth/loginfra appen med credentials. - Server udsteder access token (10-15 min levetid) og refresh token (fx 30 dage).
- Appen gemmer begge i sin sikre storage (Keychain på iOS, Keystore på Android).
- Alle API-kald bruger
Authorization: Bearer <access_token>. - Når access token udløber, kalder appen
/auth/refreshmed refresh token. - Ved logout slettes tokens i appen, og refresh token markeres som revokeret på serveren.
Hvis du også har en web-frontend, kan du lade den bruge samme API, men med refresh token i cookie (som beskrevet før). Så har du én fælles auth-logik på serversiden.
Hvordan du ikke maler dig op i et hjørne
Så hvor lander vi, hvis vi prøver at være bare lidt pragmatiske?
- Bygger du en klassisk webapp eller en simpel SPA til web: Start med sessions. Sæt dine cookie-flags rigtigt, brug fornuftig TTL, og kom videre med features.
- Ved du at du skal have mobilapps og flere klienter: Giv dig selv tiden til at lære JWT-setup ordentligt, med rotation og ordentlig storage.
- Uanset hvad du vælger: Planlæg logout og revocation fra dag 1. Det er her forskellen på “okay” og “av” viser sig senere.
Hvis du sidder og tænker “det her virker som meget arbejde bare for login”, så har du ret. Men det er stadig mindre arbejde end at forklare en vred bruger, hvorfor deres stjålne token aldrig rigtig dør.
Og ja, en kedelig session-cookie med et godt indeks i databasen er oftere den voksne løsning end et perfekt designet, stateless JWT-cirkus du alligevel ikke har brug for.








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