I januari 2026 beslutade Integritetsskyddsmyndigheten (IMY) att döma ut en sanktionsavgift på 6 miljoner kronor mot Sportadmin i Skandinavien AB. Orsaken var en SQL-injektionssårbarhet som utnyttjats redan i januari 2025, där en angripare kom åt och senare publicerade uppgifter om över 2,1 miljoner personer, många av dem barn, på darknet. Fallet är ett av flera nordiska exempel på vad som händer när autentisering och åtkomstkontroll i ett API brister.

Problemet är inte unikt för Sverige. Enligt Akamais rapportering registrerades omkring 150 miljarder API-attacker mellan januari 2023 och december 2024, och incidenter kopplade till OWASP API Security Top 10 ökade med 32 procent under samma period. I en senare Akamai-undersökning uppgav 87 procent av tillfrågade organisationer att de drabbats av minst en API-relaterad säkerhetsincident under 2025, med en snittkostnad på 580 000 dollar per incident. Salt Security har separat rapporterat att 99 procent av organisationerna stötte på API-säkerhetsproblem under det senaste året, att 29 procent handlade om svag autentisering, och att hela 95 procent av attackerna kom från källor som redan hade en giltig (eller stulen) autentiseringstoken.

Den sista siffran är den viktigaste att ta med sig. Ett API som bara kontrollerar att en JWT-token är giltig, utan att verifiera algoritm, utfärdare eller vem token faktiskt tillhör, öppnar dörren för precis den typen av attacker. I den här tutorialen bygger vi en produktionsnära API-säkerhetslösning i Node.js baserad på JSON Web Tokens (JWT), steg för steg, med utgångspunkt i OWASP API Security Top 10:2023 och IETF:s RFC 8725 (JWT Best Current Practices). Målet är ett API som klarar objektnivåauktorisering, funktionsnivåauktorisering, rate limiting, nyckelrotation och tokenåterkallelse, inte bara inloggning.

Guiden är skriven för utvecklare och tekniska team som redan har ett Express-baserat API igång, eller som planerar ett nytt, och som vill gå från “vi har inloggning” till “vi kan visa exakt hur autentiseringen skyddar varje endpoint”. Vi går igenom 14 konkreta steg, från projektuppsättning till automatiserade tester, och avslutar med en lista över vanliga fallgropar och åtta felsökningsscenarier du sannolikt stöter på när du kopplar ihop flera tjänster kring samma nyckelpar.

Vad är JWT och hur fungerar tokenbaserad autentisering

En JSON Web Token består av tre base64url-kodade delar separerade med punkter: header, payload och signatur. Headern anger vilken algoritm som använts för signeringen, payloaden innehåller claims som sub (användar-id), iss (utfärdare), aud (mottagare) och exp (utgångstid), och signaturen bevisar att innehållet inte manipulerats sedan servern skapade token. Till skillnad från en traditionell sessions-cookie är JWT stateless, servern behöver inte slå upp sessionen i en databas för varje request. Det gör JWT snabbt att verifiera, men det medför också ett problem: en utfärdad token är giltig tills den går ut, oavsett vad som händer på serversidan däremellan.

Ett vanligt missförstånd är att JWT är krypterad. Det är den inte, som standard. Base64url är kodning, inte kryptering, och vem som helst kan klistra in en token på jwt.io och läsa hela payloaden i klartext. Signaturen bevisar bara att innehållet inte manipulerats, den döljer det inte. Lägg därför aldrig lösenord, personnummer eller andra känsliga uppgifter direkt i en JWT-payload. Behöver du kryptering också finns JWE (JSON Web Encryption) som ett separat lager, men för de flesta API:er räcker det att hålla payloaden till id:n och roller, och hämta känslig data separat efter att token verifierats.

Access token och refresh token

De flesta produktionssystem delar upp autentiseringen i två tokens. Access token är kortlivad, oftast 5-15 minuter, och skickas med varje API-anrop i en Authorization: Bearer-header. Refresh token är långlivad, ofta 7-30 dagar, lagras säkrare (till exempel i en httpOnly-cookie) och används enbart för att hämta en ny access token när den gamla gått ut. Den uppdelningen begränsar skadan om en access token läcker, samtidigt som användaren slipper logga in på nytt varje kvart. Vi implementerar båda delarna i den här guiden, inklusive rotation av refresh tokens vid varje förnyelse.

Ett API-nyckel-baserat skydd (en statisk nyckel i en header) räcker för interna verktyg med få användare, men skalar dåligt när du behöver veta exakt vem som gör anropet, vilken roll personen har och när åtkomsten ska upphöra. JWT löser det genom att bära den informationen i själva token, kryptografiskt signerad så att den inte går att förfalska utan tillgång till den privata nyckeln. Tabellen nedan sammanfattar de claims vi använder genomgående i den här guiden.

ClaimNamnExempel i denna guide
subSubject (ämne)Användarens unika id, t.ex. databas-id
issIssuer (utfärdare)https://auth.dittforetag.se
audAudience (mottagare)https://api.dittforetag.se
expExpiration (utgångstid)10 minuter för access token, 7 dagar för refresh
kidKey ID (header, inte payload)Pekar ut vilken publik nyckel som ska användas vid verifiering
rollEgen (icke-standard) claimAnvänds i vår funktionsnivåauktorisering i steg 8

Förutsättningar: verktyg och versioner

Du behöver följande innan du börjar. Versionerna nedan är de senaste stabila utgåvorna i augusti 2026.

VerktygVersionSyfte
Node.js24.19.0 LTS (Krypton)Körmiljö, aktiv LTS till 20 oktober 2026
npmmedföljer Node 24.xPakethantering
expresssenaste 4.x/5.x från npmHTTP-server och routing
jsonwebtoken9.0.3Signering och verifiering av JWT
express-rate-limit8.6.2Rate limiting mot inloggning och refresh
bcrypt eller argon2senaste från npmLösenordshashning
Redis7.x (valfritt)Blocklista för återkallade refresh-tokens
curl eller Postmanvalfri versionTesta API:et manuellt

Du bör redan kunna grunderna i JavaScript och Express. Har du inte satt upp lösenordshashning tidigare är vår guide till lösenordshashning i Node.js ett bra komplement innan du fortsätter, eftersom vi förutsätter att lösenord aldrig lagras i klartext.

OWASP API Security Top 10:2023 i korthet

OWASP driver ett separat Top 10-projekt för API:er, skilt från det generella webbapplikations-Top 10 (som uppdaterades till OWASP Top 10:2025 med Broken Access Control som ny etta och Authentication Failures på sjunde plats). API-listan är mer specifik och pekar ut var JWT-baserad autentisering oftast går fel. Akamais 2026-rapport visar att felkonfiguration, trasig objekt- och egenskapsnivåauktorisering (BOPLA) och trasig autentisering var de vanligaste sårbarheterna under 2025, där felkonfiguration ensamt stod för nästan 40 procent av observerade problem.

KodNamnVad det innebär
API1:2023Broken Object Level AuthorizationEn användare kan komma åt andras objekt genom att byta id i URL:en (BOLA)
API2:2023Broken AuthenticationSvag eller felaktigt validerad inloggning, t.ex. dålig JWT-verifiering
API3:2023Broken Object Property Level AuthorizationAPI:et exponerar eller låter användare ändra fält de inte borde nå
API4:2023Unrestricted Resource ConsumptionIngen rate limiting, angripare kan överbelasta eller kostnadsdränera tjänsten
API5:2023Broken Function Level AuthorizationVanlig användare kan anropa admin-funktioner (BFLA)
API6:2023Unrestricted Access to Sensitive Business FlowsAffärslogik (t.ex. köp, bokning) kan automatiseras och missbrukas
API7:2023Server Side Request ForgeryAPI:et kan luras att göra anrop åt angriparen mot interna system
API8:2023Security MisconfigurationStandardinställningar, öppna felmeddelanden, saknad CORS-policy
API9:2023Improper Inventory ManagementGamla eller odokumenterade API-versioner lämnas kvar och glöms bort
API10:2023Unsafe Consumption of APIsData från andra API:er litas på utan validering

Fyra av dessa kategorier, API1, API2, API4 och API5, går direkt att åtgärda i hur du bygger och verifierar JWT. Det är också de vi lägger mest krut på i stegen nedan.

Hotmodell: tre angrepp den här guiden skyddar mot

Innan vi börjar bygga är det värt att ringa in exakt vilka attacker vi försöker stoppa, eftersom det styr vilka kontroller som är obligatoriska och vilka som är extra skydd. Det första scenariot är algoritmförvirring, där en angripare byter ut headerns alg-fält och försöker få servern att verifiera en falsk token med fel nyckeltyp. Det andra är stulna eller läckta tokens, där en giltig access- eller refresh-token hamnar hos fel person genom XSS, ett loggfel eller en komprometterad klientenhet. Salt Securitys siffra om att 95 procent av attackerna kommer från redan autentiserade källor handlar till stor del om just det här scenariot.

Det tredje scenariot är auktoriseringsluckor som Sportadmin-fallet, där tokenverifieringen fungerar perfekt men applikationslogiken glömmer att kontrollera vem som faktiskt äger resursen som efterfrågas. Det är viktigt att skilja på de här tre, eftersom en organisation som bara löser det första (till exempel genom att byta till RS256) fortfarande kan vara sårbar för det tredje. Stegen nedan är ordnade så att varje steg täcker ett specifikt scenario, inte i en godtycklig ordning.

Steg 1-2: Projektuppsättning och grundserver

Steg 1. Skapa ett nytt projekt och installera beroenden. Vi använder RS256 (asymmetrisk signering) istället för HS256 rakt av, eftersom en delad hemlighet mellan för många tjänster ökar risken vid läckage.

mkdir sakert-api && cd sakert-api
npm init -y
npm install express [email protected] [email protected] bcrypt helmet cors cookie-parser ioredis
npm install --save-dev nodemon

Steg 2. Bygg en minimal Express-grundserver med säkra standardheaders via helmet och en strikt CORS-policy. Undvik cors({ origin: "*" }) i produktion, det räknas som API8:2023, säkerhetsmisskonfiguration.

// server.js
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
const cookieParser = require("cookie-parser");

const app = express();

app.use(helmet());
app.use(express.json());
app.use(cookieParser());
app.use(cors({
  origin: ["https://app.dittforetag.se"],
  credentials: true
}));

app.get("/health", (req, res) => res.json({ status: "ok" }));

app.listen(3000, () => console.log("API igång på port 3000"));

Steg 3-4: Användarmodell, lösenordshashning och tokengenerering

Steg 3. Lagra aldrig lösenord i klartext eller med enkla hashalgoritmer som MD5 eller SHA-256 utan salt. Använd bcrypt eller Argon2. Vill du gå djupare på val av kostnadsfaktor och migrering mellan algoritmer finns detaljerna i vår guide om lösenordshashning i Node.js. Här är principen i korthet:

const bcrypt = require("bcrypt");

async function skapaAnvandare(losenord) {
  const hash = await bcrypt.hash(losenord, 12);
  return hash; // lagra hash, aldrig losenord
}

async function verifieraLosenord(losenord, hash) {
  return bcrypt.compare(losenord, hash);
}

Steg 4. Generera ett RSA-nyckelpar för RS256-signering. Den privata nyckeln stannar på autentiseringsservern, den publika nyckeln kan delas fritt med andra tjänster som behöver verifiera tokens.

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
const jwt = require("jsonwebtoken");
const fs = require("fs");

const privateKey = fs.readFileSync("private.pem");

function skapaTokens(anvandare) {
  const accessToken = jwt.sign(
    { sub: anvandare.id, roll: anvandare.roll },
    privateKey,
    {
      algorithm: "RS256",
      expiresIn: "10m",
      issuer: "https://auth.dittforetag.se",
      audience: "https://api.dittforetag.se",
      keyid: "2026-08-nyckel-1"
    }
  );

  const refreshToken = jwt.sign(
    { sub: anvandare.id, typ: "refresh" },
    privateKey,
    { algorithm: "RS256", expiresIn: "7d", issuer: "https://auth.dittforetag.se" }
  );

  return { accessToken, refreshToken };
}

Steg 5-6: JWT-verifieringsmiddleware enligt RFC 8725

Steg 5. Det klassiska alg: "none"-angreppet fungerar när en server accepterar vilken algoritm som helst från tokenets header, inklusive ingen signering alls. RFC 8725 kräver att bibliotek och applikationer uttryckligen anger vilka algoritmer som är tillåtna, och aldrig litar på vad token själv påstår. Med jsonwebtoken löser du det med en explicit algorithms-lista i verify(), aldrig genom att läsa headern och agera på den manuellt.

const jwt = require("jsonwebtoken");
const fs = require("fs");
const publicKey = fs.readFileSync("public.pem");

function verifieraToken(req, res, next) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith("Bearer ")) {
    return res.status(401).json({ fel: "Saknad Authorization-header" });
  }

  const token = auth.split(" ")[1];

  try {
    const payload = jwt.verify(token, publicKey, {
      algorithms: ["RS256"],           // lita aldrig pa headerns alg
      issuer: "https://auth.dittforetag.se",
      audience: "https://api.dittforetag.se",
      clockTolerance: 5                 // sekunder, hanterar klockskillnad
    });
    req.anvandare = payload;
    next();
  } catch (err) {
    return res.status(401).json({ fel: "Ogiltig eller utgangen token" });
  }
}

module.exports = verifieraToken;

Steg 6. Notera att verifieringen ovan kontrollerar både issuer och audience, inte bara signaturen. Det stoppar ett scenario där en giltig token utfärdad för en tjänst återanvänds mot en annan, ett vanligt sätt att kringgå API2:2023, trasig autentisering, i miljöer med flera mikrotjänster.

Steg 7-8: Stoppa BOLA och BFLA med korrekt auktorisering

Steg 7. En giltig JWT bevisar vem användaren är, inte vad den får göra. Sportadmin-incidenten och liknande fall handlar ofta om att applikationen litar på ett id i URL:en utan att kontrollera ägarskap. Det är API1:2023, Broken Object Level Authorization, och det är enligt Salt Securitys statistik en av de vanligaste orsakerna bakom faktiska produktionsincidenter. Lösningen är en middleware som jämför resursens ägare med den inloggade användarens id, för varje anrop, inte bara vid inloggning, och som körs efter verifieraToken men innan själva routehanteraren exekverar någon databaslogik.

async function kravAgarskap(req, res, next) {
  const resurs = await Dokument.findById(req.params.id);
  if (!resurs) return res.status(404).json({ fel: "Hittades inte" });

  if (resurs.agarId !== req.anvandare.sub) {
    return res.status(403).json({ fel: "Ingen atkomst till denna resurs" });
  }
  req.resurs = resurs;
  next();
}

app.get("/dokument/:id", verifieraToken, kravAgarskap, (req, res) => {
  res.json(req.resurs);
});

Steg 8. Funktionsnivåauktorisering (API5:2023, BFLA) skyddar admin-endpoints från vanliga användare, även om de har en giltig token. Bygg en rollkontroll som en separat middleware, återanvändbar över hela API:et.

function kravRoll(tillatnaRoller) {
  return (req, res, next) => {
    if (!tillatnaRoller.includes(req.anvandare.roll)) {
      return res.status(403).json({ fel: "Otillracklig behorighet" });
    }
    next();
  };
}

app.delete("/anvandare/:id", verifieraToken, kravRoll(["admin"]), raderaAnvandare);

Steg 9-10: Rate limiting och rotation av refresh-tokens

Steg 9. API4:2023, obegränsad resursförbrukning, drabbar ofta login- och refresh-endpoints hårdast eftersom de är enkla mål för brute force och credential stuffing. Sätt striktare gränser där än på resten av API:et. En fast tidsruta (fixed window) är enklast att implementera men kan släppa igenom dubbelt så många anrop precis vid gränsen mellan två fönster, en glidande tidsruta (sliding window) ger jämnare skydd men kostar något mer i minne och beräkning. För inloggning och refresh är skillnaden sällan avgörande, express-rate-limit använder fast fönster som standard och det räcker gott i de flesta fall.

const rateLimit = require("express-rate-limit");

const inloggningsBegransning = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { fel: "For manga inloggningsforsok, forsok igen om 15 minuter" }
});

app.post("/auth/login", inloggningsBegransning, hanteraInloggning);
app.post("/auth/refresh", inloggningsBegransning, hanteraRefresh);

Steg 10. Refresh tokens bör roteras vid varje användning, och den gamla ska ogiltigförklaras direkt. Det gör att en stulen refresh-token som redan använts en gång inte längre fungerar, och du kan upptäcka återanvändning som en varningssignal. En snabb blocklista i Redis räcker gott för de flesta system.

const Redis = require("ioredis");
const redis = new Redis();

async function hanteraRefresh(req, res) {
  const gammal = req.cookies.refreshToken;
  const aterkallad = await redis.get(`aterkallad:${gammal}`);

  if (aterkallad) {
    // tecken pa stold token, aterkalla hela sessionen
    return res.status(401).json({ fel: "Token redan anvand, session avslutad" });
  }

  const payload = jwt.verify(gammal, publicKey, { algorithms: ["RS256"] });
  await redis.set(`aterkallad:${gammal}`, "1", "EX", 60 * 60 * 24 * 7);

  const { accessToken, refreshToken } = skapaTokens({ id: payload.sub, roll: payload.roll });
  res.cookie("refreshToken", refreshToken, { httpOnly: true, secure: true, sameSite: "strict" });
  res.json({ accessToken });
}

Steg 11-12: Nyckelrotation med JWKS och säkra headers

Steg 11. RFC 8725 rekommenderar att varje nyckel binds till exakt en algoritm och att du har en rutin för att byta nycklar utan driftstopp. Exponera dina publika nycklar via en JWKS-endpoint (JSON Web Key Set), och märk varje token med vilken nyckel (kid) den signerades med. På så sätt kan verifierande tjänster slå upp rätt nyckel automatiskt, även mitt under en rotation.

app.get("/.well-known/jwks.json", (req, res) => {
  res.json({
    keys: [
      { kid: "2026-08-nyckel-1", kty: "RSA", use: "sig", alg: "RS256", n: "...", e: "AQAB" },
      { kid: "2026-05-nyckel-0", kty: "RSA", use: "sig", alg: "RS256", n: "...", e: "AQAB" }
    ]
  });
});

Steg 12. Skydda refresh-token-cookien med httpOnly, secure och sameSite: "strict", och lagra aldrig access token i localStorage där den är läsbar för skadlig JavaScript vid en XSS-attack. Kombinera med helmet() för säkra standardheaders och en uttrycklig CORS-lista, inte en wildcard. Lägg gärna till en Content-Security-Policy som begränsar vilka domäner som får köra script i din frontend, det är ett direkt skydd mot just de XSS-attacker som annars kan komma åt en token lagrad i localStorage. helmet() sätter rimliga standardvärden för de flesta av dessa headers, men CSP behöver oftast justeras manuellt efter vilka externa resurser din applikation faktiskt laddar.

Steg 13: Testa API:et och tolka svaren

Testa hela flödet med curl innan du kopplar in ett frontend. Så här ser en lyckad inloggning ut:

curl -i -X POST https://api.dittforetag.se/auth/login \
  -H "Content-Type: application/json" \
  -d '{"epost":"[email protected]","losenord":"mycket-langt-losenord"}'

# Forvantat svar:
# HTTP/1.1 200 OK
# {"accessToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtMDgt..."}
curl -i https://api.dittforetag.se/dokument/482 \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

# Om anvandaren inte ager dokument 482:
# HTTP/1.1 403 Forbidden
# {"fel":"Ingen atkomst till denna resurs"}

Ett 403-svar på ett objekt du inte äger, istället för 404 eller 200 med fel data, är precis det beteende som skyddar mot API1:2023. Testa gärna att manuellt byta id i URL:en till ett du vet tillhör en annan användare, det är det enklaste sättet att verifiera att din BOLA-middleware faktiskt fungerar innan release.

Steg 14: Automatiserade tester för auktorisering

Manuell curl-testning i steg 13 fångar de uppenbara felen, men BOLA-buggar smyger sig ofta in igen efter en refaktorering om ingen automatiserad test bevakar dem. Skriv minst ett test per skyddad endpoint som uttryckligen försöker komma åt en annan användares resurs och förväntar sig 403, inte 200 eller 404. Med Jest och supertest ser ett sådant test ut ungefär så här:

const request = require("supertest");
const app = require("../server");

test("nekar atkomst till annans dokument (BOLA)", async () => {
  const tokenForAnna = await loggaInOchHamtaToken("[email protected]");

  const svar = await request(app)
    .get("/dokument/999") // 999 tillhor en annan anvandare
    .set("Authorization", `Bearer ${tokenForAnna}`);

  expect(svar.status).toBe(403);
});

test("nekar admin-endpoint for vanlig anvandare (BFLA)", async () => {
  const tokenForAnna = await loggaInOchHamtaToken("[email protected]");

  const svar = await request(app)
    .delete("/anvandare/12")
    .set("Authorization", `Bearer ${tokenForAnna}`);

  expect(svar.status).toBe(403);
});

Kör dessa tester i CI-pipelinen vid varje pull request, inte bara lokalt. Om ett team lägger till en ny endpoint utan att koppla på kravAgarskap eller kravRoll, är det ett sådant test som ska stoppa den från att nå produktion, inte en manuell kodgranskning som kan missa det.

Loggning, övervakning och NIS2/GDPR-krav i Sverige

Sportadmin-fallet slutade inte bara med en sanktionsavgift. IMY:s beslut byggde delvis på att bolaget inte kunnat visa att man upptäckt eller loggat den avvikande åtkomsten i tid. Sedan 2024 omfattas fler svenska verksamheter av NIS2-direktivets krav på incidentrapportering, vilket i praktiken betyder att du måste kunna svara på vem som anropade vilken endpoint, med vilken token och när, om något går fel. Logga därför strukturerat: token-id (inte hela token), sub-claim, endpoint, statuskod och tidsstämpel för varje 401- och 403-svar, och skicka de loggarna till ett system separat från applikationsservern så att en angripare som tar över servern inte kan radera spåren.

Ett plötsligt uppsving i 403-svar mot samma resurs-id, eller samma användare som får 401 upprepade gånger från olika IP-adresser inom loppet av minuter, är två av de enklaste signalerna att larma på. Har din organisation redan en incidenthanteringsplan kan du koppla dessa larm direkt dit, se vår guide om NIS2-incidentplan i 12 steg för hur rapporteringskraven ser ut i praktiken. Behöver du bredare kontext om hur svenska dataintrång faktiskt sker och upptäcks finns det i vår genomgång av Verizon DBIR 2026.

Vanliga fallgropar vid JWT-implementation

  • Lagra access token i localStorage. Skadlig JavaScript från en enda XSS-lucka kan läsa ut token direkt. Använd httpOnly-cookies för känsliga tokens där det går.
  • För långa livstider på access tokens. En token giltig i 24 timmar ger en angripare gott om tid om den läcker. Håll access tokens korta, 5-15 minuter, och luta dig mot refresh-flödet för resten.
  • Ingen algorithms-lista i verify(). Att låta biblioteket gissa algoritm utifrån tokenens header öppnar för alg-confusion-attacker, där en RS256-nyckel missbrukas som HS256-hemlighet.
  • Glömd validering av issuer och audience. Utan den kontrollen kan en token utfärdad för en tjänst återanvändas mot en annan i samma mikrotjänstlandskap.
  • Ingen rate limiting på autentiseringsendpoints. Login och refresh är de mest attraktiva målen för brute force, ändå är det ofta där skyddet saknas helt.
  • Saknad objektnivåauktorisering. Att bara kontrollera att token är giltig, utan att kontrollera ägarskap till resursen, är exakt det mönster som låg bakom Sportadmin-fallet.
  • Hemligheter och privata nycklar i git-historiken. En privat RSA-nyckel som råkat committas ger fullständig kontroll över tokenutfärdandet, även efter att filen tagits bort.

Felsökning: 8 vanliga problem och lösningar

De flesta felen nedan dyker upp första gången du kopplar ihop flera tjänster kring samma nyckelpar, eller när koden flyttar från lokal utveckling till en produktionsmiljö med lastbalanserare och proxyservrar. Gå igenom listan innan du felsöker från grunden, chansen är stor att du känner igen symptomet direkt.

ProblemTrolig orsakLösning
JsonWebTokenError: invalid signatureFel publik nyckel eller algoritm-mismatch mellan signering och verifieringKontrollera att rätt public.pem laddas och att algorithms matchar den privata nyckelns typ
TokenExpiredError trots ny tokenKlockskillnad (clock skew) mellan servrarSynka servertid med NTP, höj clockTolerance till 5-10 sekunder
401 trots giltig token i klientenSaknad “Bearer “-prefix eller header med fel namnVerifiera exakt headerformat: Authorization: Bearer <token>
CORS-fel vid refresh-anropSameSite/secure-inställning på cookien fungerar inte i lokal utvecklingAnvänd sameSite: "lax" och http i dev, strict och https i produktion
Rate limit drabbar legitima användareMånga användare bakom samma NAT/proxy delar IP-adressSätt app.set("trust proxy", 1) korrekt och begränsa per användar-id istället för enbart IP
JWKS-endpoint svarar långsamtIngen caching av nycklar hos konsumerande tjänsterCacha JWKS-svaret i minuter, inte sekunder, och invalidera vid ny kid
Giltiga refresh-tokens nekas efter rotationRace condition när två requests förnyar samtidigtLägg en kort gracetid (några sekunder) innan den gamla token markeras återkallad
Redis-blocklistan växer okontrolleratSaknad TTL på återkallade tokensSätt alltid EX till samma livstid som tokenens expiresIn

Prestanda: vad kostar JWT-verifiering i praktiken

En vanlig invändning mot asymmetrisk signering är att RSA-operationer är tyngre än en enkel HMAC-beräkning. Det stämmer för signering, men inte lika mycket för verifiering, vilket är det som sker vid varje enskilt API-anrop. Verifiering med en publik RSA-nyckel är betydligt billigare beräkningsmässigt än att signera med den privata, och för de flesta API:er är det inte JWT-verifieringen som blir flaskhalsen, det är databasanropen som sker efter att token godkänts.

Vill du pressa svarstiderna ytterligare finns två enkla åtgärder. Cacha den publika nyckeln eller JWKS-svaret i minnet istället för att läsa filen eller göra ett nätverksanrop för varje request, och undvik att slå upp användarens fullständiga profil i databasen bara för att verifiera token, låt istället claims som roll bära den information som behövs för själva auktoriseringsbeslutet. Byt du till ES256 istället för RS256 får du dessutom kortare signaturer och nycklar, vilket märks marginellt i nätverkstrafik om du kör API:er med väldigt hög anropsfrekvens.

Signeringsalgoritmer: HS256, RS256 och ES256

Valet av algoritm påverkar både säkerhet och driftskomplexitet. HS256 är enklast att komma igång med men kräver att alla parter delar samma hemlighet, vilket blir en svag punkt om du har många mikrotjänster. RS256 och ES256 använder nyckelpar, den privata nyckeln stannar hos utfärdaren och bara den publika delas ut.

AlgoritmTypPassar bäst för
HS256Symmetrisk (delad hemlighet)Enskild monolit där en och samma tjänst signerar och verifierar
RS256Asymmetrisk (RSA-nyckelpar)Mikrotjänster där flera tjänster bara behöver verifiera, inte signera
ES256Asymmetrisk (elliptisk kurva)Samma användningsfall som RS256, men med kortare nycklar och signaturer

Enligt RFC 8725 ska varje nyckel bindas till exakt en algoritm, och kontrollen ska ske vid själva kryptooperationen, inte genom att lita på vad tokenens header påstår. Det är därför den explicita algorithms-listan i steg 5 är obligatorisk, inte valfri.

Avancerade tips för produktion

När grundskyddet sitter finns flera lager att bygga vidare på. Mellan interna tjänster kan mTLS (mutual TLS) komplettera JWT genom att kräva klientcertifikat utöver token, vilket gör stulna tokens betydligt mindre användbara på egen hand. DPoP (Demonstrating Proof of Possession) är en nyare teknik som binder en access token till en specifik klientnyckel, så att en avlyssnad token inte går att återanvända från en annan enhet.

För extremt känsliga flöden, som betalningar eller administratörsåtkomst, är opaque tokens med server-side introspektion ibland ett säkrare val än JWT, just eftersom de går att återkalla omedelbart utan blocklistor. Om din applikation redan stödjer WebAuthn kan du med fördel kombinera passkeys för själva inloggningen med JWT för efterföljande API-anrop, se vår guide om passkeys (WebAuthn) i Node.js för implementationsdetaljer. Sätt även upp strukturerad loggning av alla 401- och 403-svar, ett plötsligt uppsving i nekade anrop mot samma resurs-id är ofta det första tecknet på ett pågående BOLA-försök.

Överväg också en gateway eller service mesh (Kong, Envoy, eller en molnleverantörs API-gateway) som terminerar och validerar tokens innan trafiken ens når din applikationskod. Det centraliserar algoritm- och claim-kontrollerna på ett ställe istället för att varje enskild mikrotjänst behöver implementera samma logik korrekt, vilket i sig minskar risken för att en tjänst glöms bort och blir kvar med svagare skydd, exakt det som OWASP beskriver som API9:2023, bristfällig inventariehantering.

Komplett fungerande projekt: filstruktur

Sätter du ihop alla steg ovan får du en projektstruktur ungefär så här:

sakert-api/
  private.pem
  public.pem
  server.js
  middleware/
    verifieraToken.js
    kravAgarskap.js
    kravRoll.js
  routes/
    auth.js
    dokument.js
  lib/
    tokens.js
    redis.js
  package.json

Den här strukturen täcker autentisering (JWT-generering och verifiering), auktorisering (objekt- och funktionsnivå), rate limiting och tokenrotation, alltså fyra av de fem vanligaste kategorierna i OWASP API Security Top 10:2023. Nästa naturliga steg är att lägga till automatiserade tester som specifikt försöker kringgå varje kontroll, till exempel genom att byta resurs-id eller skicka en token med fel aud-claim, innan koden går till produktion.

Lägg aldrig private.pem i git. Lägg filnamnet i .gitignore direkt när du skapar nyckelparet, och läs in sökvägar via miljövariabler (PRIVATE_KEY_PATH, JWT_ISSUER, JWT_AUDIENCE) istället för att hårdkoda dem i server.js. I produktion bör den privata nyckeln ligga i en hemlighetshanterare som AWS Secrets Manager, Azure Key Vault eller HashiCorp Vault, inte som en fil på disk i containern. Skulle en nyckel ändå råka committas, räcker det inte att ta bort filen i en ny commit, den finns kvar i historiken och måste roteras omedelbart, med en ny kid publicerad i JWKS-endpointen innan den gamla nyckeln tas bort helt.

Det här är samma grundproblem som låg bakom Sportadmin-fallet i utgångspunkten för den här guiden, fast vänt åt andra hållet. Där saknades kontrollen som skyddar en resurs från fel användare. Här handlar det om att skydda själva nyckeln som hela autentiseringssystemet vilar på. Bägge är exempel på att API-säkerhet sällan handlar om en enda funktion man glömt, utan om att flera lager, giltig token, rätt algoritm, rätt ägarskap och begränsad livstid, behöver hålla samtidigt för att systemet ska vara säkert i praktiken.

Vanliga frågor om API-säkerhet och JWT

Vad är skillnaden mellan JWT och sessionsbaserad autentisering?

Sessioner lagras på servern och slås upp vid varje request, vilket gör dem enkla att återkalla direkt. JWT är stateless och verifieras utan databasslagning, vilket är snabbare men kräver extra mekanismer som blocklistor för att kunna återkallas i förtid.

Är JWT säkrare än cookies?

Det är fel jämförelse, JWT är ett tokenformat och kan lagras i en cookie. Säkerheten avgörs av hur token lagras och skickas, inte av formatet i sig. En JWT i en httpOnly-cookie med secure och sameSite är betydligt säkrare än samma token i localStorage.

Hur länge bör en access token vara giltig?

5-15 minuter är en vanlig utgångspunkt för produktionssystem. Kortare livstid minskar skadan om token läcker, men kräver ett fungerande refresh-flöde så att användaren inte märker av förnyelsen.

Kan man återkalla en JWT innan den går ut?

Inte direkt, eftersom verifieringen är stateless. Lösningen är en blocklista (till exempel i Redis) över återkallade token-id:n, som kontrolleras i verifieringssteget, precis som i refresh-rotationen vi byggde i steg 10.

RS256 eller HS256, vilket ska jag välja?

Välj RS256 eller ES256 så snart mer än en tjänst behöver verifiera tokens. HS256 fungerar för en enskild monolit, men blir en risk att hantera säkert i takt med att fler tjänster behöver dela samma hemlighet, eftersom varje ny tjänst som får hemligheten är ytterligare en plats den kan läcka från. Byter du senare från HS256 till RS256 måste alla klienter och tjänster uppdateras samtidigt, så det är enklare att börja rätt än att migrera i efterhand.

Vad är BOLA och varför är det farligt?

Broken Object Level Authorization innebär att API:et kontrollerar att token är giltig, men inte att användaren faktiskt äger den specifika resurs som efterfrågas. Det är den vanligaste kategorin i OWASP API Security Top 10 och exakt det mönster som låg bakom Sportadmin-incidenten i Sverige. Testa alltid genom att logga in som en användare och sedan manuellt ändra ett resurs-id i URL:en till ett du vet tillhör någon annan, precis som i testet vi skrev i steg 14.

Behöver jag JWT om jag redan använder OAuth2?

OAuth2 är ett auktoriseringsramverk, inte ett tokenformat. JWT används ofta som access token inom ett OAuth2-flöde, så de två kompletterar varandra snarare än konkurrerar.

Ska passkeys ersätta JWT helt?

Nej, de löser olika problem. Passkeys (WebAuthn) ersätter lösenordet vid själva inloggningen, medan JWT fortsätter hantera vem som är inloggad under efterföljande API-anrop. De två går utmärkt att kombinera.

Räcker HTTPS för att skydda JWT under transport?

HTTPS skyddar token mot avlyssning på nätverksnivå, men skyddar inte mot att token stjäls på klienten via XSS eller mot att servern felaktigt litar på headerns algoritm. Se HTTPS som ett obligatoriskt grundskydd, inte en ersättning för korrekt verifiering och auktorisering.

Måste ett JWT-relaterat intrång anmälas till IMY?

Ja, om ett brutet API-skydd leder till att personuppgifter läcker eller blir åtkomliga för fel person räknas det som en personuppgiftsincident enligt GDPR, och den ska normalt anmälas till Integritetsskyddsmyndigheten inom 72 timmar. Sportadmin-fallet visar dessutom att en dokumenterad, fungerande auktoriseringskontroll och tydlig loggning är precis det som väger tungt när IMY bedömer om sanktionsavgiften ska bli stor eller liten. Bolag som redan omfattas av NIS2 har dessutom kortare interna deadlines för att larma internt, vilket gör den strukturerade loggningen i tidigare avsnitt till mer än bara ett tekniskt “nice to have”.

Relaterad läsning

Källor och vidare läsning: OWASP API Security Top 10:2023, OWASP Top 10, RFC 8725, JSON Web Token Best Current Practices, Curity: JWT Best Practices samt Integritetsskyddsmyndighetens nyhetsarkiv.