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.
| Claim | Namn | Exempel i denna guide |
|---|---|---|
| sub | Subject (ämne) | Användarens unika id, t.ex. databas-id |
| iss | Issuer (utfärdare) | https://auth.dittforetag.se |
| aud | Audience (mottagare) | https://api.dittforetag.se |
| exp | Expiration (utgångstid) | 10 minuter för access token, 7 dagar för refresh |
| kid | Key ID (header, inte payload) | Pekar ut vilken publik nyckel som ska användas vid verifiering |
| roll | Egen (icke-standard) claim | Anvä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.
| Verktyg | Version | Syfte |
|---|---|---|
| Node.js | 24.19.0 LTS (Krypton) | Körmiljö, aktiv LTS till 20 oktober 2026 |
| npm | medföljer Node 24.x | Pakethantering |
| express | senaste 4.x/5.x från npm | HTTP-server och routing |
| jsonwebtoken | 9.0.3 | Signering och verifiering av JWT |
| express-rate-limit | 8.6.2 | Rate limiting mot inloggning och refresh |
| bcrypt eller argon2 | senaste från npm | Lösenordshashning |
| Redis | 7.x (valfritt) | Blocklista för återkallade refresh-tokens |
| curl eller Postman | valfri version | Testa 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.
| Kod | Namn | Vad det innebär |
|---|---|---|
| API1:2023 | Broken Object Level Authorization | En användare kan komma åt andras objekt genom att byta id i URL:en (BOLA) |
| API2:2023 | Broken Authentication | Svag eller felaktigt validerad inloggning, t.ex. dålig JWT-verifiering |
| API3:2023 | Broken Object Property Level Authorization | API:et exponerar eller låter användare ändra fält de inte borde nå |
| API4:2023 | Unrestricted Resource Consumption | Ingen rate limiting, angripare kan överbelasta eller kostnadsdränera tjänsten |
| API5:2023 | Broken Function Level Authorization | Vanlig användare kan anropa admin-funktioner (BFLA) |
| API6:2023 | Unrestricted Access to Sensitive Business Flows | Affärslogik (t.ex. köp, bokning) kan automatiseras och missbrukas |
| API7:2023 | Server Side Request Forgery | API:et kan luras att göra anrop åt angriparen mot interna system |
| API8:2023 | Security Misconfiguration | Standardinställningar, öppna felmeddelanden, saknad CORS-policy |
| API9:2023 | Improper Inventory Management | Gamla eller odokumenterade API-versioner lämnas kvar och glöms bort |
| API10:2023 | Unsafe Consumption of APIs | Data 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.
| Problem | Trolig orsak | Lösning |
|---|---|---|
| JsonWebTokenError: invalid signature | Fel publik nyckel eller algoritm-mismatch mellan signering och verifiering | Kontrollera att rätt public.pem laddas och att algorithms matchar den privata nyckelns typ |
| TokenExpiredError trots ny token | Klockskillnad (clock skew) mellan servrar | Synka servertid med NTP, höj clockTolerance till 5-10 sekunder |
| 401 trots giltig token i klienten | Saknad “Bearer “-prefix eller header med fel namn | Verifiera exakt headerformat: Authorization: Bearer <token> |
| CORS-fel vid refresh-anrop | SameSite/secure-inställning på cookien fungerar inte i lokal utveckling | Använd sameSite: "lax" och http i dev, strict och https i produktion |
| Rate limit drabbar legitima användare | Många användare bakom samma NAT/proxy delar IP-adress | Sätt app.set("trust proxy", 1) korrekt och begränsa per användar-id istället för enbart IP |
| JWKS-endpoint svarar långsamt | Ingen caching av nycklar hos konsumerande tjänster | Cacha JWKS-svaret i minuter, inte sekunder, och invalidera vid ny kid |
| Giltiga refresh-tokens nekas efter rotation | Race condition när två requests förnyar samtidigt | Lägg en kort gracetid (några sekunder) innan den gamla token markeras återkallad |
| Redis-blocklistan växer okontrollerat | Saknad TTL på återkallade tokens | Sä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.
| Algoritm | Typ | Passar bäst för |
|---|---|---|
| HS256 | Symmetrisk (delad hemlighet) | Enskild monolit där en och samma tjänst signerar och verifierar |
| RS256 | Asymmetrisk (RSA-nyckelpar) | Mikrotjänster där flera tjänster bara behöver verifiera, inte signera |
| ES256 | Asymmetrisk (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
- OWASP Top 10 i Node.js: 12 steg, 30 min
- Lösenordshashning i Node.js: 12 steg, 45 min
- Tvåfaktorsautentisering i Node.js: 12 Steg
- Passkeys (WebAuthn) i Node.js: 14 Steg, 60 Min
- ChatGPT API: Stoppa Prompt Injection i 12 Steg
- NIS2-incidentplan: 12 Steg, 3-5 Dagar
- Verizon DBIR 2026: 22 000 dataintrång analyserade
- Fler artiklar om cybersäkerhet
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.




