Jeg fortrød ikke sessions før min tredje JWT-ulykke

Jeg fortrød ikke sessions før min tredje JWT-ulykke

Det korte svar er at du næsten altid kan starte med sessions i 2026. Men det længere svar er mere interessant.

Jeg opdagede det på den hårde måde en tirsdag aften, hvor jeg sad med en halvfærdig SPA, tre forskellige token-typer og en hjerne der mest af alt lignede localStorage: fuld af rod, ingen udløbsdato og nul struktur.

Hvis du sidder og googler “sessions vs JWT” lige nu, er du sandsynligvis samme sted: du skal bygge login til en webapp, alle tutorials råber noget forskelligt, og du vil gerne vælge noget der ikke bider dig om et år.

Hvad du egentlig prøver at løse (og hvorfor det ikke starter med JWT)

De fleste starter med spørgsmålet “skal jeg bruge JWT?”. Det er lidt som at starte et boligkøb med “skal jeg have altan?”. Det er ikke helt det rigtige niveau.

Start hellere med: hvad er det faktisk for en app du bygger, og hvem skal logge ind hvordan?

Spørgsmål 1: Hvem er klienten?

Her er de typiske varianter jeg ser hos begyndere og let øvede:

  • Klassisk webapp: serveren renderer HTML (f.eks. Django, Laravel, Rails, Next.js med serverrendering) og brugeren klikker rundt.
  • SPA (Single Page Application): React/Vue/Svelte frontend der taler med et JSON API.
  • Ren API: backend uden frontend, fx til en mobilapp eller andre systemer.
  • Hybrid: lidt serverrendering, lidt SPA, måske en mobilapp senere.

Allerede her ryger vi tæt på svaret:

  • Klassisk webapp: sessions er næsten altid det kærligste valg.
  • SPA: sessions kan stadig være det bedste, især med en BFF (Backend For Frontend).
  • Ren API til andre systemer: her begynder JWT at give mening.

Hvis du er i tvivl om du overhovedet bør bygge en tung SPA, kan du godt lige tage et kig på hvordan du vælger mellem SPA og klassisk webapp før du går videre. Det valg påvirker hele din auth-strategi.

Spørgsmål 2: Hvor mange klienter og domæner skal du understøtte?

Sessions fungerer bedst, når tingene er nogenlunde simple:

  • En webapp.
  • Et domæne.
  • Brugere der logger ind i browseren.

JWT begynder først at give reel mening når du har:

  • Flere forskellige klienter (web, mobil, måske tredjepartsintegrationer).
  • Brugere og systemer der skal tale med dit API uden en browser.
  • Behov for at andre systemer selv kan validere tokens uden at ringe til din server hver gang.

Hvis du lige nu bare vil bygge en portfolio-app, en lille SaaS eller et hobbyprojekt, er alt det her med “stateless” og “microservices” typisk bare ekstra kompleksitet. Du kommer hurtigt til at bygge et mini-auth-system du slet ikke får brug for.

Sessions forklaret i praksis: det kedelige system der bare virker

Sessions lyder gammeldags, men i virkeligheden er det bare et mønster:

  1. Brugeren logger ind med brugernavn og kodeord.
  2. Serveren tjekker credentials og opretter en session i et server-side store (fx database, Redis).
  3. Serveren sender en session cookie tilbage til browseren (f.eks. sessionid=abc123).
  4. Browseren sender automatisk den cookie med på fremtidige requests til samme domæne.
  5. Serveren slår sessionen op og ved hvem du er.

Ingen tokens der flyver rundt i JavaScript eller localStorage. Ingen signaturer du selv skal validere. Du slår bare et ID op et sted.

Hvad er en session-cookie egentlig?

En session-cookie er bare en almindelig HTTP cookie med nogle ekstra flags. De vigtige:

  • HttpOnly: din JavaScript-kode kan ikke læse den.
  • Secure: den sendes kun over HTTPS.
  • SameSite: styrer hvor nemt den kan misbruges i CSRF-angreb.

Et godt sæt defaults i 2026:

  • HttpOnly = true
  • Secure = true (aldrig plain HTTP i produktion)
  • SameSite = Lax eller Strict til klassisk webapp

Du kan tænke på sessionen som et kort i garderoben: brugeren får bare et nummer, selve jakken hænger sikkert inde bagved.

Logout der faktisk gør noget

En af grundene til at jeg stadig er ret glad for sessions:

Når du logger ud, kan serveren slette eller invalidere sessionen i sit store. Den cookie der ligger i browseren, peger så bare på noget der ikke findes mere.

Det føles måske banalt, men i JWT-land er logout tit bare “vi venter på at token udløber”, og så begynder man at opfinde blacklists og ekstra storage som man netop prøvede at slippe for.

JWT forklaret i praksis: hvornår de faktisk giver mening

JWT står for JSON Web Token. Det er i bund og grund et signeret objekt, du kan sende rundt mellem systemer. Det består af tre dele:

  • Header: metadata, fx “alg”: “HS256”.
  • Payload: data om brugeren (fx { "sub": "123", "role": "admin" }).
  • Signature: en kryptografisk signatur af de to første dele.

De tre dele er base64-kodet og sat sammen med punktummer, fx:

eyJhbGciOi...eyJzdWIiOiIxMjM"...SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Serveren kan validere signaturen og tjekke at payload ikke er ændret. Den behøver ikke slå noget op i en database for at vide hvem du er. Det er her “stateless” narrativet kommer fra.

Hvornår JWT er stærke

JWT giver mening når du har:

  • Mange forskellige services der skal stole på det samme login.
  • API-kald fra andre systemer, hvor du ikke vil have cookies i spil.
  • Behov for at tredjeparts-klienter kan validere tokens uden at ringe til dig.

Eksempel: Du har et API der både bruges af din egen mobilapp, et partner-dashboard og en ekstern integration. Her kan et signeret token være praktisk: de kan alle validere tokenet, uden at du har et centraliseret session-store.

Hvornår JWT er overkill (og lidt farlige)

Hvis du bare har:

  • En React-app der taler med en Node backend.
  • En Next.js app med lidt client-side features.
  • En lille hobby-SaaS hvor alt går gennem din backend.

… så får du typisk flere problemer af JWT end du løser.

De klassiske fælder:

  • Tokens ender i localStorage hvor XSS kan læse dem.
  • Refresh tokens bliver også smidt i browseren.
  • Logout betyder “vi krydser fingre og venter på at tokens udløber”.
  • Du bygger et refresh-system der er sværere end hele resten af appen.

Det er her jeg selv lavede mine “JWT-ulykker”: jeg brugte dem længe før jeg faktisk havde brug for dem.

5 scenarier og hvad jeg selv ville vælge i 2026

Nu bliver vi konkrete. Forestil dig de her fem typer projekter. Hvad vil give dig flest point for mindst hovedpine?

1) Klassisk webapp med serverrendering

Eksempel: Django-app, Laravel, Rails, eller et Next.js-projekt der mest server-renderer HTML og kun har lidt JavaScript på siderne.

Mit valg: Sessions med HttpOnly cookies.

  • Session-store i database eller Redis.
  • SameSite=Lax eller Strict.
  • CSRF-token som hidden field i forms eller header.

Det er her sessions er stærkest. Du kan logge brugeren ud server-side, du har serveren i loopet på alle requests, og du behøver ikke opfinde flere tokens.

2) SPA i frontend, eget backend-API (monolit)

Eksempel: React/Vue app der taler med en Node/Express eller Django REST backend, men alt kører på samme domæne.

Mit valg: Stadig sessions, men med en BFF-agtig tilgang.

To muligheder:

  • Host SPA og backend på samme domæne, brug HttpOnly session-cookie.
  • Brug en lille BFF (Backend For Frontend) som proxy mellem SPA og API, så browseren kun har cookies til BFF’en.

SPA’en laver så bare fetch-kald uden at tænke på tokens. Browseren sender automatisk session-cookien.

Hvis du kan slippe for JWT her, gør det. Dit liv bliver markant roligere. Og du kan bruge tiden på at lære om frontend frameworks i stedet for at debugge CORS og token-udløb.

3) Ren API til andre systemer

Eksempel: Du bygger et API som andre tjenester eller scripts skal kunne bruge. Ingen browser, ingen cookies, måske CLI scripts eller server-til-server.

Mit valg: JWT eller andre tokens, men ikke sessions.

Her giver det mening at have et signeret token per klient eller bruger:

  • Access tokens med relativt kort udløb.
  • Evt. refresh tokens, men ikke i en browser.
  • Scopes i payload, så du kan styre hvad et token må.

Server-til-server betyder ingen XSS, ingen cookies, ingen CSRF. Helt andet trusselsbillede, og JWT føles pludselig mere naturligt.

4) Mobilapp + webapp

Eksempel: Du har en React Native app og en webapp, der begge skal tale med det samme backend-API.

Mit valg:

  • Webapp: sessions med HttpOnly cookies.
  • Mobilapp: tokens (f.eks. JWT) gemt sikkert i appens storage-muligheder.

Det kræver typisk at du har et API der kan håndtere begge dele:

  • Browser requests med cookies.
  • Mobil requests med Authorization header (Bearer <token>).

Det lyder mere skræmmende end det er. Det er ofte bare to forskellige “auth middlewares” der kører samme sted.

5) Multi-tenant, flere domæner, avanceret arkitektur

Hvis du er her, og du stadig læser Coding Class, så er du enten meget ambitiøs eller allerede ansat et sted.

Scenarier som:

  • Flere custom-domæner per kunde.
  • Microservices der hver især skal validere auth.
  • Tredjepartsintegrationer du ikke helt stoler på.

Mit valg: En kombi. Ofte:

  • JWT eller lignende mellem services.
  • Sessions og cookies ind mod brugernes browsere.

Hvis du bare er ved at bygge dine første begynderprojekter, så parkér det her i baghovedet. Du behøver ikke bygge til “fremtidig multi-tenant enterprise” i dit første login.

Sikkerheds-tjekliste: minimum secure defaults i 2026

Okay, uanset om du vælger sessions eller JWT, er der nogle basis-ting du bør gøre.

Cookies

  • Altid Secure i produktion.
  • Altid HttpOnly for sessions og følsomme cookies.
  • SameSite=Lax som udgangspunkt. Skift til Strict hvis du kan, og til None hvis du skal dele cookies på tværs af domæner (med Secure).

CSRF-strategi

Hvis du bruger cookies som auth (sessions eller access tokens i cookies), skal du tænke CSRF. To typiske mønstre:

  • CSRF-token i form-data eller headers, som serveren tjekker.
  • SameSite cookies der blokerer de værste cross-site requests.

Mange frameworks har CSRF indbygget. Brug dem. Og hvis du en dag skal bygge cookie-bannere, så lav dem rigtigt fra start, der er en hel historie om det i artiklen om cookie-bannere der ikke er snyd.

Token storage (hvis du ender med JWT)

Hvis du vil bruge JWT i en browser-app:

  • Gem aldrig tokens i localStorage eller sessionStorage.
  • Brug HttpOnly cookies til tokens, og tænk CSRF med ind.
  • Hold access tokens korte (f.eks. 5-15 minutter).

Refresh tokens bør som udgangspunkt ikke ligge i browseren. Hvis du alligevel gør det, bevæger du dig ind i et hjørne hvor du skal forstå en del mere om XSS, device-beskyttelse og revocation.

Rotation og udløb

  • Sessions: giv dem en udløbstid og forny den ved aktivitet.
  • JWT: brug exp claim, og lav rotation for refresh tokens hvis de findes.

Ingen tokens eller sessions bør leve for evigt. Brugere glemmer logins åbne, laptops bliver stjålet, faner står åbne på caféer. Ja, også på min.

Typiske fejl (jeg har lavet dem alle)

1) “Jeg smider bare token i localStorage, det er jo nemt”

Ja, det er nemt. Det er også det nemmeste sted for en XSS til at stjæle det.

Hvis du har et sted i din app hvor brugeren kan skrive HTML-lignende ting (kommentarer, profiler, hvad som helst), og du en eller anden dag får en XSS, så ligger der pludselig et gyldigt token på hylden, klar til at blive stjålet.

2) “Stateless” som religiøst mål

Jeg havde en periode hvor jeg var helt forelsket i idéen om at min backend var 100 % stateless. Ingen session-store, ingen server-side state, alt i tokens.

Det gik fint. Lige indtil jeg skulle:

  • Tilbagekalde adgang for en bruger.
  • Logge nogen ud på tværs af devices.
  • Gennemføre et password-reset som invalidere alt gammelt.

Pludselig havde jeg lavet et endnu mere komplekst sessionsystem, bare med JWT ovenpå.

3) Refresh tokens i browseren uden trusselsmodel

Refresh tokens har længere levetid. De er din “nøgle til nye nøgler”.

Hvis du lægger dem i browseren, skal du have gennemtænkt:

  • Hvordan du opdager misbrug.
  • Hvordan du tilbagekalder dem.
  • Hvordan du håndterer devices der ligger og laver stille refresh.

Det er ikke begynderniveau. Og hvis du er nået hertil som ny udvikler, vil jeg næsten hellere anbefale at bruge et gennemprøvet auth-system eller starte med sessions og holde det simpelt.

Minimal implementeringsplan for begge valg

Lad os sige du skal bygge login til en lille webapp. Her er to ruter.

Rute A: Sessions til klassisk webapp eller SPA + BFF

  1. Login endpoint
    POST /login med brugernavn og kodeord. Tjek mod database.
  2. Opret session
    Lav en record i din session-tabel: id, user_id, expires_at, evt. user-agent og IP.
  3. Sæt cookie
    Svar med en Set-Cookie header:
    Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
  4. Middleware
    På alle requests læser du cookie, slår session op og finder bruger.
  5. Logout
    DELETE /session eller POST /logout der sletter session-record.

Hvis du har en SPA, kalder den bare dit backend-endpoint. Browseren holder styr på cookies.

Rute B: JWT til ren API eller mobilklient

  1. Login endpoint
    POST /login med brugernavn og kodeord. Tjek mod database.
  2. Udsted access token
    Lav et JWT med payload som fx:
    {
      "sub": "user-id-123",
      "iat": 1719945600,
      "exp": 1719949200,
      "scope": "read:posts write:posts"
    }
    Signer med en stærk hemmelighed (HS256) eller privat nøgle (RS256).
  3. Svar til klient
    Mobil/CLI: send access token tilbage i JSON.
    { "access_token": "<jwt-her>", "token_type": "Bearer" }
  4. Brug af token
    Klienten sender:
    Authorization: Bearer <jwt-her>
  5. Middleware
    Backend validerer signaturen, tjekker exp og finder bruger-id i sub.

Refresh tokens kan komme oveni, men hvis du kan leve med at brugeren logger ind lidt oftere, så lad være. Mindre kompleksitet, færre ting at beskytte.

Når du skal skifte strategi uden at slette alt

Det fede ved auth-valg er, at de kan ændres senere. Ikke gratis, men det kan lade sig gøre uden at rive hele huset ned.

Fra sessions til JWT

Typisk forløb:

  • Behold sessions til browser-logins.
  • Tilføj et nyt endpoint til at udstede API-tokens/JWT til specifikke klienter.
  • Lad dine nye klienter (fx scripts, partnerapps) bruge tokens.

Senere kan du flytte mere af logikken over på tokens, hvis det giver mening.

Fra JWT over det hele til sessions i browseren

Det her har jeg selv gjort på en lille hobby-side der var blevet et sikkerhedsmareridt. Ja, den historie er fortalt et andet sted som en hel artikel om fail-sikkerhed.

Planen så nogenlunde sådan her ud:

  1. Indfør server-side session-store parallelt med eksisterende JWT-auth.
  2. Opdater login, så det både udsteder session og JWT.
  3. Skift frontend til at bruge endpoints der bygger på sessions (cookies) i stedet for Authorization-header.
  4. Fjern brugen af JWT i browseren, men behold dem til evt. API-clients.

På den måde kunne jeg trække langsomt i retning af noget mere simpelt uden at stoppe hele appen.

Så hvad gør du nu?

Hvis jeg skal koge min erfaring med “sessions vs JWT” ned til én sætning, er det denne her:

Vælg det mindst komplekse system der stadig er sikkert nok til dit faktiske use case.

For de fleste små webapps i 2026 betyder det: sessions med HttpOnly cookies, fornuftige SameSite-indstillinger og en simpel CSRF-strategi.

JWT er ikke onde. De er bare et værktøj som for alvor viser sin styrke, når du har flere klienter, services og systemer i spil. Hvis du er i læringsfasen, får du typisk mere ud af at forstå cookies, headers og basis websikkerhed for udviklere, end af at bygge et mini-OAuth-system selv.

Og den tirsdag aften med tre slags tokens? Jeg endte med at skrue det hele tilbage til en almindelig session-cookie, slettede 300 linjer “smart” refresh-logik og gik i seng en time tidligere. Jeg savnede ikke mine JWT-eksperimenter en eneste gang bagefter.

JWT er som standard stateless, så du får ikke automatisk revokation. Brug korte levetider for access-tokens og opbevar refresh-tokens sikkert (fx httpOnly cookies) så du kan tilbagekalde dem i en server-side liste eller ved at rotere refresh-tokens. Alternativt brug opaque tokens eller en introspektion/revokations-endpoint for central kontrol.
Sæt cookie-flags: Secure, HttpOnly hvor muligt og SameSite=Lax eller Strict for at begrænse cross-site requests. Til stadie-ændrende handlinger brug en server-verificeret CSRF-token eller double-submit cookie, og undgå at lægge auth-tokens i adgangelige steder som localStorage.
Undgå localStorage/sessionStorage hvis du kan få XSS, da scripts der kører på siden kan læse tokens. Foretræk httpOnly, Secure cookies eller hold tokens i memory og brug en httpOnly cookie til at hente nye tokens via en sikker refresh-endpoint (BFF-mønster er ofte enklere).
Planlæg en gradvis migration: tilføj en token-udstedende endpoint og understøt begge mekanismer parallelt, så browserbrugere kan blive på sessions mens mobile/eksterne klienter bruger JWT. Brug en mapping mellem session-id og token-id eller en central revokationsliste for at gøre logout og revokation konsistente under overgangen.

Ida Balslev er den type ven, der pludselig dukker op i din messenger med et link til en lille web-app, hun lige har bygget for sjov – og bagefter gerne viser dig, hvordan du selv kan lave den. Hendes passion for kodning startede med en hjemmebygget hjemmeside til en hestestald og er langsomt vokset gennem aftener med tutorials, fejlmeldinger og små, hjemmelavede projekter.

På Codingclass.dk deler Ida den viden, hun selv manglede i starten: konkrete eksempler, tydelige forklaringer og ærlige historier om, hvad der typisk går galt første, anden og tredje gang. Hun elsker at tage et abstrakt begreb som fx "API" eller "asynkron JavaScript" og koge det ned til noget, du kan se, klikke på og lege med i browseren. For hende handler kodning ikke om at være perfekt, men om at turde prøve, bryde ting og bygge dem op igen.

Ida skriver især om webudvikling med HTML, CSS og JavaScript, små Python-scripts og grundlæggende koncepter som debugging, versionsstyring og struktur i din kode. Hun tænker altid i næste skridt: når du først forstår idéen, viser hun dig, hvordan du kan udvide det med en ekstra funktion, lidt pænere styling eller en smartere måde at tænke din kode på.

Gennem sine artikler på Codingclass.dk vil Ida gerne give dig følelsen af, at du ikke sidder alene med koden – men at der faktisk er en, der har kæmpet med de samme fejlmeddelelser og nu gerne vil vise dig en vej igennem dem, i et tempo hvor alle kan være med.

Send kommentar

You May Have Missed