Stop med at gemme tokens tilfældigt i din webapp

Stop med at gemme tokens tilfældigt i din webapp

Det er som at gemme husnøglen under dørmåtten

At bygge auth i en webapp føles lidt som at forlade sin lejlighed: du skal ud, men du vil også gerne kunne komme ind igen. De fleste vælger så bare at smide nøglen under dørmåtten. Det vil sige: localStorage + JWT uden at tænke videre over det.

Det virker, indtil nogen finder din måtte.

I denne artikel går vi fra det helt basale: hvad er egentlig hemmeligt, hvad kan stjæles, og hvor giver det mening at opbevare tokens, API-nøgler og sessions. Målet er ikke at gøre dig til sikkerhedsekspert, men at give dig en arbejdende mental model, så du ikke laver de klassiske begynder-fejl.

Hvad er faktisk hemmeligt i en webapp?

Lad os starte pinligt simpelt. Der er typisk tre ting, folk forveksler:

  • API keys
  • Tokens (fx JWT)
  • Sessions

API keys: ikke magiske passwords

En API key er ofte bare en identifikator: “den her app bruger min service”. Nogle API keys er følsomme, andre er mere som et brugernavn.

Typisk:

  • Public API keys til fx frontends, analytics eller maps: må godt ligge i frontend, men stadig ikke i git, hvis det kan undgås.
  • Secret API keys til betaling, mail-udsendelse osv.: skal på backend, væk fra browseren.

Regel: Alt der giver direkte adgang til penge, data eller konti, skal leve på serveren.

Tokens: din midlertidige adgangsbillet

Et token (ofte JWT) er et stykke data, serveren har signeret. Det siger typisk “bruger X er logget ind, og det gælder til tidspunkt Y”.

Hvis nogen får fat i dit token:

  • Kan de udgive sig for at være dig, indtil tokenet udløber.
  • Kan de bruge API’et fra deres egen maskine, ikke kun fra din browser.

Regel: Tokens skal behandles som passwords light. Ikke helt så følsomme (de kan udløbe), men tæt på.

Sessions: reference til noget på serveren

Sessioner er klassisk model: serveren gemmer en række data om dig (bruger-id, rolle, osv.), og du får et session-id i en cookie.

Hvis nogen får session-id’et, kan de misbruge det på samme måde som et token. Forskellen er mest teknisk: dataen bor på serveren, ikke i selve cookie-værdien.

Frontend er aldrig hemmelig

Det her er en sætning, jeg synes alle frontend-udviklere burde have printet på deres skærm:

Alt, hvad der er i frontend-koden, er ikke hemmeligt.

Det gælder:

  • JavaScript-filer (bundle, source maps, alt der bliver sendt ned)
  • Byggetid-env variabler som havner i bundlen (fx VITE_API_KEY)
  • Tokens du gemmer i localStorage eller sessionStorage

Ikke hemmeligt betyder: brugeren, deres udvidelser og deres malware kan læse det.

Så spørgsmålet er ikke “kan jeg skjule den her hemmelighed i frontend?”. Det kan du ikke. Spørgsmålet er: “hvad sker der, hvis nogen får det her?”.

Trusselsmodel på 5 linjer: hvad prøver vi at beskytte imod?

Sikkerhed er nemmere, hvis du kender dine fjender. De tre klassikere her:

  • XSS (Cross-Site Scripting)
  • CSRF (Cross-Site Request Forgery)
  • Token theft (ren tyveri af tokens/sessions)

XSS: angreb gennem din egen JavaScript

XSS sker, når en angriber får lov til at køre JavaScript i din side. Typisk via:

  • Brugerinput du renderer som HTML uden at escape.
  • 3. parts scripts du inkluderer ukritisk.

Hvad kan de så?

  • Læse localStorage og sessionStorage.
  • Læse ikke-httpOnly cookies.
  • Kalde dit API med dine tokens.

Husk: Hvis der er XSS, er spillet næsten tabt. Det handler mest om at begrænse skaden. Her kommer storage-valg ind i billedet.

CSRF: angreb gennem din browser som mellemmand

CSRF er mere oldschool. Forestil dig:

  1. Du er logget ind på bank.dk i en fane.
  2. Du besøger ond-side.dk i en anden fane.
  3. ond-side.dk laver en POST-request til bank.dk/overfoersel.

Fordi din browser automatisk sender cookies med til bank.dk, kan requesten se legitim ud, selvom du aldrig trykkede på “overfør”.

CSRF handler altså mest om cookies der bliver sendt automatisk. Tokens i localStorage sendes ikke automatisk. De er mere udsatte for XSS, til gengæld er de ikke automatisk sårbare for CSRF.

Token theft: nogen får bare fat i nøglen

Det kan være:

  • Nogen ser din skærm / får fat i din maskine.
  • Et browser-plugin lækker dine localStorage-værdier.
  • Du logger ind på en fælles computer og glemmer at logge ud.

Her hjælper ting som:

  • Kort udløbstid på access tokens.
  • Mulighed for at logge alle sessions/tokens ud server-side.

Cookies vs localStorage vs memory: hvordan vælger du?

Nu til det, de fleste faktisk googler: hvor gemmer man tokens sikkert i en webapp.

Der er 3 realistiske muligheder:

  • Cookies
  • localStorage / sessionStorage
  • Memory (kun i JavaScript-variabler)

Cookies: gode til serverstyret auth

Cookies bliver automatisk sendt med ved hvert request til et domæne. Det er både styrken og svagheden.

Fordele:

  • Virker med klassisk server-renderede sider.
  • Med httpOnly kan JavaScript ikke læse værdien.
  • Serveren kan styre login helt uden JS i browseren.

Ulemper:

  • Sårbare for CSRF, hvis de ikke er konfigureret ordentligt.
  • Lidt mere opsætning end bare at kalde localStorage.setItem.

localStorage / sessionStorage: nemt, men XSS-venligt

De her er fristende. API’et er simpelt, og alt er tilgængeligt for din JavaScript.

Fordele:

  • Meget nemt at arbejde med i SPA-frameworks.
  • Kontrol over hvornår tokens sendes til API’et.
  • Ingen automatisk CSRF, fordi tokens ikke sendes automatisk.

Ulemper:

  • Alle værdier er læsbare ved XSS-angreb.
  • Kan læses af browser-extensions.

Hvis du vælger localStorage, har du ubevidst valgt: “jeg tror ikke, jeg får XSS”. Det er et ret stort løfte at give.

Memory only: det midlertidige kompromis

Memory betyder: du gemmer tokens i en variabel i JavaScript, fx i en React context eller et state management bibliotek.

Fordele:

  • Efter reload/tab-lukning er token væk, så angrebsvindue er mindre.
  • Kan stadig ikke læses uden XSS, men så er du lige så ramt som med localStorage.

Ulemper:

  • Brugeren skal logge ind igen når fanen lukkes.
  • Kræver lidt mere arbejde at bygge login-flowet.

En simpel beslutningsregel

Hvis du bygger en lille portfolio- eller hobby-app, vil jeg typisk sige:

  • Til klassisk login med egen backend: httpOnly cookie med session eller kortlivet JWT.
  • Til rene frontend-projekter mod 3. parts API: hvis API’et understøtter public keys / OAuth implicit flow, så brug det og forenkl din auth.
  • Til simple demoer uden rigtige brugere: localStorage er ok, men vær bevidst om tradeoffs.

httpOnly, secure og sameSite forklaret uden volapyk

Når du sætter cookies, kan du tilføje en håndfuld flag. De ser tekniske ud, men de gør faktisk noget brugbart.

httpOnly: skjul for JavaScript

httpOnly betyder: cookie kan kun sendes via HTTP requests. JavaScript kan ikke læse den via document.cookie.

Effekt:

  • XSS kan ikke bare lave document.cookie og stjæle din session direkte.
  • De kan stadig bruge din session så længe scriptet kører, men det er sværere at eksfiltrere tokenet.

secure: kun over HTTPS

secure betyder: cookie sendes kun over HTTPS. Aldrig over HTTP.

Det er et minimumskrav i produktion. Hvis du sender auth-cookies over HTTP på et offentligt netværk, er det svarende til at dele Wi-Fi med halv maratonfeltet og håbe ingen kigger.

sameSite: dit våben mod CSRF

sameSite styrer, hvornår cookies sendes med på cross-site requests.

  • sameSite=Lax: sendes ved navigation fra andre sites (fx link), men ikke ved baggrunds-requests som <img>, form POST osv.
  • sameSite=Strict: sendes kun, når du er direkte på siden.
  • sameSite=None: sendes altid, også cross-site, men kræver secure.

Til en klassisk enkel webapp på eget domæne er Lax ofte et fint sted at lande.

Hvor gemmer du API-nøgler fornuftigt?

Du har måske allerede læst artiklen på Coding Class om environment variables og .env-filer. Hvis ikke, er pointen her kort:

Hemmelige API keys bor på serveren, ikke i frontend.

Mønsteret: backend som proxy

Hvis du har et hemmeligt API, du vil tilgå fra en frontend, så lav en lille backend der:

  1. Modtager en request fra din frontend.
  2. Tilføjer den hemmelige API key på serversiden.
  3. Kalder det eksterne API.
  4. Sender kun de nødvendige data tilbage til frontend.

API key’en ligger som environment variable på serveren, ikke i bundlen.

Hvor ligger env vars så?

Typisk:

  • I hostingens UI (Netlify, Vercel, Railway osv.).
  • Som secrets i GitHub Actions / CI.

Du har måske set mønstret allerede i fx en lille Node/Express-server:

import 'dotenv/config'
import express from 'express'

const app = express()

app.get('/weather', async (req, res) => {
  const apiKey = process.env.WEATHER_API_KEY
  // kald 3. parts API med apiKey
})

Opskrift: session-cookie setup i menneskesprog

Lad os tage et klassisk session-setup. Du har en backend (fx Node/Express, Django, Laravel eller hvad du nu er faldet over), og du vil bare have “log ind og hold mig logget ind”.

Flowet i grove træk

  1. Bruger sender brugernavn + password til /login.
  2. Serveren tjekker credentials.
  3. Serveren opretter en session i en database / in-memory store.
  4. Serveren sender en session-cookie tilbage (med et tilfældigt session-id).
  5. Browseren sender automatisk cookie med på efterfølgende requests.

Session-data (hvem du er, roller, osv.) ligger på serveren. Cookie indeholder bare en reference, typisk et tilfældigt id.

Hvad sætter du på cookie’en?

  • httpOnly: ja, næsten altid på auth-cookies.
  • secure: ja i produktion (HTTPS).
  • sameSite=Lax: godt standardvalg til simple apps.
  • maxAge / expires: hvor længe skal man være logget ind.

Do / don’t for sessions

Do:

  • Gem kun et tilfældigt id i cookie, ikke hele brugeren.
  • Invalider sessioner på serveren ved logout.
  • Brug HTTPS i produktion.

Don’t:

  • Gem adgangskoder eller følsomme ting i session-objektet.
  • Gem samme session-id i både cookie og localStorage “for en sikkerheds skyld”.
  • Del session-store mellem udviklings- og produktionsmiljø uden at vide hvad du laver.

Opskrift: JWT med access + refresh tokens

Hvis du bygger noget mere API-first, så møder du hurtigt JWT. Typisk med to tokens:

  • Access token: kort levetid (fx 5-15 minutter), bruges til API-kald.
  • Refresh token: længere levetid (fx dage/uger), bruges kun til at få nye access tokens.

Det klassiske flow

  1. Bruger logger ind med brugernavn + password.
  2. Serveren sender et kortlivet access token + et refresh token.
  3. Access token bruges til API-kald.
  4. Når access token udløber, bruger du refresh token til at få et nyt.

Hvor opbevarer du dem?

Der er to ofte diskuterede mønstre:

1) Access token i memory, refresh token i httpOnly cookie

Mønster:

  • Refresh token gemmes i en httpOnly, secure cookie.
  • Access token gemmes i en JS-variabel (memory) efter login.

Fordele:

  • Access token lever kun mens tabben er åben.
  • Refresh token er beskyttet mod direkte læsning via JS.
  • CSRF-risiko kan styres med sameSite og CSRF-tokens.

Ulemper:

  • Lidt mere kompliceret at implementere.
  • Du skal håndtere auto-refresh når access token udløber.

2) Alt i localStorage

Mønster:

  • Access token (og ofte refresh token) gemmes i localStorage.
  • JS læser token ved hver request og sætter Authorization-header.

Fordele:

  • Meget nemt at komme i gang med.
  • Ingen CSRF-problemer, da cookies ikke bruges.

Ulemper:

  • XSS kan læse og lække alle tokens.
  • Tokens bliver liggende på maskinen, selv efter fanen lukkes.

Til seriøse projekter er min personlige smertegrænse: jeg prøver at undgå refresh tokens i localStorage. Access tokens med meget kort levetid kan være acceptabelt i nogle scenarier, men det kræver, at du har styr på XSS-hardening.

Tjekliste: 12 sikkerhedsregler til små apps

Det her er din lille code review-liste. Gå den igennem før du viser din app frem i CV’et eller smider den på produktion.

  1. Gemmer jeg hemmeligheder i frontend-kode?
    Hvis ja, kan de læses af alle. Drop det eller flyt dem til backend.
  2. Ligger der tokens i localStorage?
    Overvej om det er nødvendigt, eller om cookie + session ville være bedre.
  3. Er auth-cookies sat med httpOnly?
    Hvis ikke, spørg dig selv hvorfor.
  4. Er auth-cookies sat med secure i produktion?
    Hvis du har HTTPS, så er svaret næsten altid ja.
  5. Har jeg valgt en fornuftig sameSite værdi?
    Til simple same-domain apps er Lax fint. Skriv den eksplicit.
  6. Kan jeg logge alle sessions / tokens ud server-side?
    Fx ved at slette sessions eller invalider refresh tokens.
  7. Har mine access tokens kort udløbstid?
    Timer/dage er for langt til seriøse ting. Minutter er mere realistisk.
  8. Renser jeg brugerinput, der bliver til HTML?
    Især hvis du bruger innerHTML eller lignende.
  9. Logger jeg aldrig tokens og API keys til konsol eller logs?
    console.log(token) virker uskyldigt, indtil nogen deler screenshots.
  10. Er mine env vars sat via hosten, ikke hårdkodet i repo?
    Især for ting som database passwords og payment keys.
  11. Har jeg slået source maps fra i produktion, hvis jeg ikke behøver dem?
    Ikke en redning, men reducerer lidt information.
  12. Har jeg skelnet mellem demo-setup og rigtig produktion?
    localStorage er til quick demos, ikke til bank-apps.

Hvornår skal du få nogen sikkerheds-klogere end dig ind over?

Hvis du bare bygger en TODO-app til dig selv, er det fint at eksperimentere. Men der er nogle klare røde flag, hvor jeg personligt ville sige: nu skal en sikkerhedsinteresseret udvikler kigge med.

  • Du håndterer rigtige brugere med persondata (navn, mail, adresse).
  • Du håndterer betalinger, abonnementer eller kortdata (også via 3. part).
  • Du bygger noget til en virksomhed, der faktisk skal bruges af kunder.
  • Du laver login til noget som helst, andre mennesker læner sig op ad i deres hverdag.

Her er en god tommelfingerregel: Hvis det vil være pinligt at skulle skrive til brugerne og sige “deres data er nok lækket” hvis noget går galt, så få et review.

Det behøver ikke være et stort, dyrt firma. Det kan være en erfaren udvikler, der er vant til at tænke i trusselsmodeller. Vis dem din arkitektur, din cookie-konfiguration og dit token-flow.

Det vigtigste du kan gøre allerede nu

Hvis du kun tager én ting med dig, så lad det være den her:

Frontenden er aldrig hemmelig, så planlæg din opbevaring af tokens og API-nøgler som om en angriber allerede kan læse alt derinde.

Gem access-token i hukommelsen (en JS-variabel) med kort levetid, så det ikke ligger persistet i localStorage/sessionStorage. Gem refresh-token i en HttpOnly, Secure cookie med SameSite for at forhindre adgang fra JS, og implementer en server-side refresh-endpoint som roterer refresh-tokenet ved hver brug.
HttpOnly forhindrer JavaScript i at læse cookien, så XSS ikke direkte stjæler den. Secure kræver HTTPS, og SameSite begrænser hvornår cookien sendes cross-site, hvilket hjælper mod CSRF; brug SameSite=lax/strict efter behov og kombiner med andre CSRF-mekanismer for state-changing requests.
Revoke eller blacklist den kompromitterede token/nøgle på serveren, roter hemmeligheder og kræv genlogin for berørte brugere. Undersøg logs for misbrug, forkort token-levetider fremadrettet, og for API-nøgler udsted nye nøgler med snævre scopes og adgangsbegrænsninger.
Mod XSS: escape al output, brug templates eller frameworks som automatisk undgår injection, og deploy Content Security Policy. Mod CSRF: brug SameSite-cookies, CSRF-tokens eller double-submit-cookie mønster, og konfigurer CORS stramt så kun tillidede origins kan kalde API'et.

Lasse Falkenberg er typen, der begyndte at rode med HTML og CSS for at lave en simpel bandside – og opdagede, at det var langt sjovere at få knapperne til at virke end at stå på scenen. Siden har han kastet sig over alt fra små JavaScript-snippets til Python-scripts, der kan spare ham for kedeligt, manuelt arbejde i hverdagen.

Han har lært det meste ved at bygge ting, der lige præcis løser hans egne problemer: en lille webapp til at holde styr på brætspilsaftener, et script til at rydde op i rodede mapper, eller en enkel side til at dele noter med venner. Undervejs har han kæmpet sig gennem alle de klassiske fejl – semikolon, forkerte indrykninger og variabler, der hedder noget helt andet end man tror – og det er præcis den rejse, han deler på Coding Class.

På Coding Class skriver Lasse praktiske, jordnære guides, der tager udgangspunkt i små, konkrete opgaver: noget du kan se, teste og bygge videre på med det samme. Han elsker at bryde en opgave ned i små bidder, vise den fulde kode og forklare linje for linje, hvad der sker – inklusive de typiske bugs, du med stor sandsynlighed også støder på.

For Lasse handler kodning ikke om flotte titler eller store ord, men om følelsen af at få noget til at virke – og om at du som læser kan gå derfra med noget, du selv har bygget. Hvis du kan kende glæden ved at få en fejl til endelig at forsvinde, er du lige på bølgelængde med hans måde at lære fra sig på.

Send kommentar

You May Have Missed