Dit loginvalg er ikke neutralt – du tager faktisk parti
At vælge login er mere som at vælge lås end som at vælge farve
Du skifter ikke hoveddøren ud med en hængelås bare fordi du læste én god blogpost om hængelåse. Men sådan vælger mange login til deres webapp: én artikel om JWT, og så kører alt på tokens fra nu af.
Auth er ikke pynt. Det er måde din app holder styr på hvem brugeren er, hvor det bliver husket, og hvem du stoler på. Og “session vs JWT vs OAuth” handler i virkeligheden om præcis de tre ting.
Her får du et beslutningstræ, ikke en religion. Vi går stille og roligt igennem: hvad du faktisk vælger, hvilke scenarier der passer til hvad, minimum sikkerhed for hver løsning, og hvordan du debugger og skifter strategi uden kaos.
Hvad du i virkeligheden vælger: state, storage og trust-boundary
Når du siger “jeg vil bruge sessions” eller “jeg vil bruge JWT”, vælger du typisk tre ting på én gang. Lad os skille dem ad.
1. Hvor ligger state?
State er “hukommelsen” om at brugeren er logget ind.
- Server-side state: klassisk session. Serveren gemmer en række i en tabel eller i Redis. Klienten har kun en nøgle (session-id).
- Client-side state: typisk JWT. Hele pakken med claims (user id, roller, udløb) ligger hos klienten, signeret så den ikke kan ændres.
Valget handler direkte om, hvor nemt det er at:
- invaliderer en bestemt bruger
- skalere på tværs af flere servere
- begrænse hvilke services der må stole på login’et
2. Hvor bliver der gemt på klienten?
Her bliver det ofte rodet i tutorials. De blander format (JWT eller session-id) og storage (cookie, localStorage, memory) sammen.
Du kan groft set vælge mellem:
- HTTP-only cookie: kan ikke læses af JavaScript, sendes automatisk med til domænet.
- JS-læselig cookie: kan læses af JavaScript, stadig bundet til domænet.
- localStorage/sessionStorage: kun tilgængeligt for JavaScript, bliver aldrig sendt automatisk med requests.
- In-memory: gemt i JS-variabler, forsvinder ved reload.
De fleste sikkerhedsproblemer med “JWT” handler faktisk om forkert storage. Hvis du gemmer noget følsomt i localStorage, har du reelt sagt: “Jeg stoler på, at jeg aldrig får XSS”. Det er ambitiøst.
3. Hvem stoler på hvem (trust-boundary)?
Trust-boundary er kanten mellem de dele af dit system, der må stole på hinanden, og dem der ikke må.
- En simpel monolit (frontend + backend på samme domæne) har typisk én boundary mod internettet.
- Et system med flere microservices, mobilapps og tredjeparts-klienter har mange små grænser.
OAuth/OIDC er dybest set et sæt regler for, hvordan du udstiller og håndterer den grænse på en standardiseret måde. Hvis dit system kun har én grænse, er det ofte overkill at starte der.
Fem typiske scenarier og hvad der faktisk passer bedst
Lad os tage nogle meget konkrete typer apps. Du behøver ikke være 100 % enig i mine valg, men brug dem som anker, når du bygger dit eget beslutningstræ.
1. Klassisk server-renderet app (MPA) med loginformular
Eksempler: admin-dashboard, intern side til et lille team, simpelt SaaS med server-rendered HTML.
Mit valg: server-side sessions i HTTP-only cookies.
Flowet ser typisk sådan her ud:
- Bruger poster brugernavn + password til
/login. - Server validerer og laver en session i databasen eller Redis.
- Server sætter en
Set-Cookieheader med et tilfældigt session-id,HttpOnly,Secure,SameSite=Lax/Strict. - Alle efterfølgende requests bærer automatisk cookie’en med.
Fordelene er kedeligt gode: let at invaliderer, let at forstå, integrerer fint med CSRF-beskyttelse. Du slipper også for at opfinde dit eget token-livscyklus-system.
2. SPA + egen backend (BFF pattern)
Eksempler: React/Vue/Angular-app der taler med en Node/Java/Spring backend, hvor backenden ejer brugerdatabasen.
Her er der to hovedretninger:
- BFF + cookies: din backend fungerer som Backend-For-Frontend, og alt auth er cookie-baseret.
- JWT til backend-API: frontenden får et token og bruger det direkte i Authorization-header.
Jeg vælger næsten altid BFF + cookies til en enkelt SPA + backend.
Opsætningen ligner MPA-case, bare med en JS-app der kalder samme domæne:
- Frontend hostes på
app.ditdomæne.dk. - Backend API ligger på
app.ditdomæne.dk/api(samme origin) eller evt. subdomæne med fornuftigt CORS-setup. - Login sætter en HTTP-only session-cookie.
- SPA’en laver fetch-kald uden at kende til tokens direkte.
Du får det bedste fra to verdener: moderne frontend og klassisk, gennemprøvet session-sikkerhed. Og du undgår en stor del af CORS- og token-rod, som mange nye JavaScript-til-web projekter ellers drukner i.
3. SPA + tredjeparts auth (Auth0, Firebase, Cognito, osv.)
Her er valget ofte taget for dig: du får JWT eller lignende tokens, og login-flowet går via deres domæne.
Mit valg her: brug deres tokens, men lad dem leve så lidt som muligt i din frontend.
Typisk mønster jeg anbefaler:
- Lad tredjepartens SDK håndtere redirect, login og callback.
- På callback-punktet sender du ID-token eller authorization code til din egen backend.
- Din backend validerer det og udsteder sin egen session-cookie (igen: HTTP-only, Secure, SameSite).
- SPA’en bruger kun cookies til at tale med din backend, ikke rå tokens mod tredjeparten.
Så bruger du OAuth/OIDC til det, de er gode til: identitet på tværs af systemer. Din egen app er stadig simpel at overskue.
4. Mobilapp + API
Her er cookies mindre attraktive. Native apps har ikke en browser der håndterer dem for dig, og du har typisk et rent HTTP-API bagved.
Mit valg: short-lived JWT access token + refresh token, med ordentlig rotation.
Et nogenlunde sundt mønster ser sådan ud:
- Bruger logger ind via et OIDC-flow (browser eller in-app browser).
- Din auth-server giver appen et access token (kort levetid, fx 5-15 min) og et refresh token (længere levetid, fx dage).
- Mobilappen gemmer tokens i OS’ets sikre storage (Keychain, Keystore), ikke i en tilfældig preferences-fil.
- API’et får access token i
Authorization: Bearer <token>headeren. - Når access token udløber, bruger appen refresh token til at få et nyt access token.
Her giver JWT mere mening, fordi API’et typisk skal skalere ud og ikke dele en central sessions-database. Men du betaler med kompleksitet: rotation, blacklist/kill-switch på refresh tokens, osv.
5. Public API til andre systemer
Eksempler: du bygger et API, hvor andre virksomheder eller scripts skal kunne integrere sig.
Mit valg: OAuth2/OIDC eller i det mindste token-baseret auth uden cookies.
Her giver det mening at:
- skelne mellem bruger-baseret access (“act-as-user”) og maskin-baseret access (client credentials)
- udarbejde scopes for hvad en given token må
- have en selvstændig auth-server, hvis systemet vokser
Det kan starte småt, men tænker du bare en smule fremad, er det her OAuth/OIDC faktisk begynder at gøre livet nemmere i stedet for sværere.
Beslutningstræ: session-cookie, JWT eller OAuth/OIDC?
Lad os gøre det mekanisk. Svar på de næste spørgsmål, som om vi sad sammen og tegnede på en serviet.
Spørgsmål 1: Er din primære klient en browser?
- Ja, mest browser (MPA eller SPA): gå til spørgsmål 2.
- Nej, mest mobil eller andre servere: gå til spørgsmål 4.
Spørgsmål 2: Er frontend og backend under samme kontrol og domæne?
- Ja, samme team og samme domæne: vælg server-side session + HTTP-only cookie.
- Nej, frontend og backend er adskilt, evt. forskellige domæner: gå til spørgsmål 3.
Spørgsmål 3: Skal din backend også bruges af andre klienter?
- Nej, kun min egen SPA: brug BFF-mønster og cookies, overvej at lægge backend bag samme origin.
- Ja, også andre apps/klienter: brug access tokens (ofte JWT) til API’et, men pak det evt. ind bag en BFF for din egen SPA.
Spørgsmål 4: Er dine klienter kontrolleret af dig?
- Ja, det er mine egne mobilapps/servers: brug JWT eller lignende tokens, som du roterer og beskytter ordentligt.
- Nej, andre organisationer eller tredjeparts-klienter: kig seriøst på OAuth2/OIDC.
Hvornår giver JWT ikke dig noget ekstra?
Hvis alle disse er sande:
- Din app er én webapp (SPA eller MPA).
- Du har én backend.
- Du ejer både frontend og backend.
- Alt kører på samme domæne eller underdomæner, som du har styr på.
Så er fordelene ved JWT små i forhold til klassiske sessions. Du får bare mere kompleksitet og flere måder at lave fejl på. Der er en grund til at jeg skrev om hvor meget et dårligt første loginvalg kan forfølge dig.
Minimum sikkerhedstjek for hver løsning
Ingen af valgene er “sikre” i sig selv. De kan alle sammen bruges forsvarligt eller på den lidt kreative måde. Så her er en lille baseline per model.
Server-side sessions i cookies
Brug den her tjekliste, hver gang du sætter cookies:
- HttpOnly på session-cookie. Så JavaScript ikke kan læse den.
- Secure så den kun sendes over HTTPS.
- SameSite=Lax (eller Strict, hvis du kan leve med det) til at reducere CSRF-risiko.
- CSRF-beskyttelse på state-ændrende endpoints (tokens eller double-submit cookie mønster).
- Session store der kan smide gamle sessions ud og finde en bruger hurtigt.
Et simpelt Express-eksempel:
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 60 // 1 time
}
}));
Læg mærke til: ingen JWT, ingen magic. Bare gode standardindstillinger.
JWT til browser-klienter
Hvis du alligevel
- Gem access token i HTTP-only cookie hvis muligt, ikke i localStorage.
- Gør access token kortlivet (få minutter).
- Brug et separat refresh token, som aldrig vises til JavaScript, og kun sendes til et dedikeret refresh-endpoint.
- Hold payloaden lille: undgå at proppet alt for mange oplysninger ind i tokenet.
- Hold styr på signing keys, og roter dem ind imellem.
Og vær helt ærlig: hvis du ender med tokens i localStorage uden XSS-strategi, så er det sårbarhed først, arkitektur bagefter.
OAuth/OIDC og tokens til mobil eller tredjeparts-klienter
Her er de vigtigste punkter:
- Brug PKCE i auth-code flow, især fra public clients (mobil, SPA).
- Gem tokens i sikker storage (Keychain, Keystore), ikke i flade filer.
- Gør refresh tokens revocable: hold en liste i backend over gyldige jti’er eller lignende.
- Definer scopes og implementer dem rent.
- Log login-events ordentligt, så du kan spore misbrug.
Det her er også stedet, hvor det bliver relevant at læse op under IT-sikkerhed for udviklere og følge best practice, ikke bare én blogpost.
Minimal implementering: hvad skal med fra dag 1?
Hvis du bygger på et studieprojekt eller et hobbyprojekt, er det fristende at tænke “det er bare for sjov”. Problemet er, at de projekter ofte bliver til noget, senere.
Hvis du vælger sessions
Minimum fra dag 1:
- Tilfældige, lange session-id’er genereret af et ordentligt bibliotek.
- Sessions gemt i noget andet end processens memory (database, Redis).
- HttpOnly + Secure på cookies.
- SameSite sat til mindst Lax.
- Time-out på sessions (maxAge og/eller server-side expiry).
Hvis du vælger JWT
Minimum fra dag 1:
- Signering med en stærk secret eller privat nøgle, ikke “secret123”.
- Kort
exppå access tokens. - Validering af
aud,issogexpi API’et. - Ingen følsomme data i payload (ingen passwords, ingen personlige ting).
- En plan for hvordan du gør tokens ugyldige ved kompromittering (blacklist, rotation, change password flow).
Hvis du vælger OAuth/OIDC
Minimum fra dag 1:
- Brug etablerede libraries og en velkendt provider (eller et gennemtestet open source projekt).
- PKCE for alle public clients.
- HTTPS overalt, uden undtagelser.
- Veldefinerede redirect-URLs, ingen wildcard-domæner.
- Log på login, logout, token-udstedelse og refresh.
Debug: sådan ser du hvad der faktisk sker i DevTools
Det her er den del, der gør forskellen på “jeg gætter på auth” og “jeg kan gennemskue auth”. Du kommer til at bruge Network-fanen meget.
Trin 1: Tjek cookies
Åbn Chrome DevTools, gå til Network-fanen, og kig på et request efter login.
- Klik på et request til dit API.
- Vælg fanen Headers.
- Scroll ned til Request Headers.
- Tjek om der er en
Cookie-header, og hvad der faktisk sendes.
Gå også i Application-fanen og vælg “Cookies” i venstremenuen. Her kan du se:
- Om din session-cookie er
HttpOnly,Secureog hvilkenSameSiteden har. - Om du ved et uheld har flere cookies for samme navn og domæne.
Trin 2: Tjek Authorization-header for JWT
Hvis du kører Bearer tokens:
- Klik på et API-request.
- Under Request Headers find
Authorization. - Tjek om den ligner
Bearer <lang-streng>.
Kopier tokenet (kun i dev, lad være i produktion) og dekod det på jwt.io eller med et lille script:
const parts = token.split('.');
const payload = JSON.parse(atob(parts[1]));
console.log(payload);
Her kan du se exp, iat, aud, iss og andre claims. Hvis noget ser forkert ud, er det ofte her problemet starter.
Trin 3: Se svar fra login-endpoints
Find requestet til dit login-endpoint:
- Tjek Response Headers for
Set-Cookie. - Ser du din session-cookie blive sat? Har den de rigtige flag?
- Hvis du bruger tokens, ligger de så i svaret som JSON, og håndterer du dem korrekt i frontenden?
Hvis du har læst artiklen om at bruge Network-fanen i DevTools, så er det nu, den viden betaler sig.
Migrering: sådan skifter du strategi uden at logge alle ud
Nogle gange opdager man for sent, at man fik valgt et auth-setup, der er for besværligt at drifte. Det er ikke forbudt at skifte mening, det kræver bare lidt omtanke.
Fra JWT til sessions
Typisk scenario: du startede med SPA + API + JWT, men vil nu tilbage til en mere klassisk session-baseret model.
En glidende migrering kunne se sådan her ud:
- Tilføj sessions til din backend uden at fjerne JWT endnu.
- Lav et nyt endpoint, fx
/session-from-token, der:
app.post('/session-from-token', authWithJwt, (req, res) => {
// authWithJwt sætter fx req.user
// Opret en server-side session baseret på req.user
req.session.userId = req.user.sub;
res.send({ ok: true });
});
- Opdater din frontend, så den efter en vellykket JWT-login kalder
/session-from-tokenog derefter skifter over til at bruge cookies. - Behold understøttelse af tokens i en overgangsperiode.
- Når stort set alle brugere har fået en session, kan du begynde at udfase brugen af tokens i frontenden.
På den måde bliver eksisterende brugere ikke logget ud med det samme. De glider bare over på en ny måde at være logget ind på, ved næste interaktion.
Fra sessions til JWT
Det sker også den anden vej: du starter med en lille monolit, men vil nu have et mere token-baseret setup, fordi der kommer mobilapps og flere services.
Her kan du gøre noget tilsvarende:
- Tilføj et endpoint, der kun kan kaldes af en authenticated bruger med session, fx
/token-from-session. - Når det kaldes, udsteder du et JWT til brugeren.
app.post('/token-from-session', requireSession, (req, res) => {
const token = jwt.sign({ sub: req.session.userId }, process.env.JWT_SECRET, {
expiresIn: '15m',
audience: 'my-api',
issuer: 'my-app'
});
res.json({ accessToken: token });
});
- Lad nye klienter (mobilapps, nye frontends) bruge JWT direkte.
- Behold gamle session-klienter, indtil du er klar til at opgradere dem.
Pointen er den samme: du behøver ikke trykke på en stor rød knap og logge hele verden ud. Gør det lagvist.
Hvis du allerede har rodet dig ud i noget
Lige et par hurtige “damage control” forslag, hvis du læser det her og kan mærke, at noget i din nuværende løsning skurrer.
Du gemmer tokens i localStorage
Gør det her som minimum:
- Indfør CSP så du begrænser hvor scripts må komme fra.
- Sørg for ingen
innerHTML-baserede renderingsting uden escaping. - Gør dine tokens kortlivede (5-10 min) og brug refresh tokens mere forsigtigt.
- Planlæg en overgang til HTTP-only cookies, hvis det er browser-klienter.
Du har JWT uden exp eller med meget lang exp
Ret det i to skridt:
- Tilføj
expmed en rimelig levetid og begynd at tjekke den i dit API. - Tilføj et endpoint, der kan invaliderer aktive tokens ved fx password-skift.
Du har sessions uden Secure/HttpOnly
Det er en hurtig gevinst:
- Slå HTTPS til i prod.
- Sæt
SecureogHttpOnlypå session-cookien. - Test at login stadig virker over HTTPS.
Det er en af de der ændringer, der tager en time, men fjerner en hel klasse af trivielle angreb.
Det ene råd du skal tage med videre
Beskriv din auth-model på én side (hvem sætter hvad, hvor ligger state, hvad roterer hvornår), før du vælger værktøj, så er chancen meget større for at dit valg af session, JWT eller OAuth bliver en bevidst beslutning og ikke bare næste tutorial du faldt over.







1 kommentar