OAuth 2.0 med PKCE (Proof Key for Code Exchange) är 2026 års standardmetod för säker auktorisering i webbapplikationer. Den gamla implicita flödet försvann ur OAuth 2.0-specifikationen (RFC 6749) efter att forskning visade att kodinterception-attacker kunde stjäla åtkomsttoken direkt från redirect-URL:en. PKCE, definierat i RFC 7636, löser det problemet med ett kryptografiskt handslag som binder auktoriseringskoden till den klient som skapade den. I den här guiden bygger du ett komplett OAuth 2.0 + PKCE-flöde i Node.js och Express från grunden, steg för steg.
Vad är OAuth 2.0 och PKCE?
OAuth 2.0 är ett auktoriseringsramverk som låter en applikation begära begränsad åtkomst till en användares konto hos en tredjepartstjänst, utan att applikationen ser användarens lösenord. Flödet involverar fyra parter: resursägaren (användaren), klienten (din Node.js-app), auktoriseringsservern (t.ex. Google, GitHub, Okta) och resursservern (det API som skyddar användarens data).
PKCE lägger till ett extra lager ovanpå auktoriseringskodflödet. Utan PKCE kan en angripare som lyckas fånga upp din auktoriseringskod, till exempel via ett elakt program på samma enhet eller via ett öppet redirect, byta ut koden mot en åtkomsttoken. Med PKCE är koden värdelös utan code_verifier, en slumpmässig sträng som bara din app känner till och som aldrig skickas i klartext under auktoriseringssteget.
| Flödestyp | Säkerhetsnivå | Status 2026 | Klienttyp |
|---|---|---|---|
| Implicit Flow | Låg (token i URL) | Föråldrat, borttaget i OAuth 2.1 | Public clients |
| Authorization Code | Medel (utan PKCE) | Acceptabelt för konfidentiella klienter | Confidential clients |
| Authorization Code + PKCE | Hög | Rekommenderat för alla klienttyper | Alla |
| Client Credentials | Hög (maskin-till-maskin) | Aktivt | Confidential clients |
| Device Code | Medel | Aktivt för IoT och CLI-appar | Enhetsbegränsade klienter |
OAuth 2.1-utkastet, som kombinerar erfarenheterna från RFC 6749, RFC 7636 och RFC 9700 (OAuth 2.0 Security Best Current Practice), kräver PKCE för alla auktoriseringskodflöden och tar bort den implicita flödet helt. Att implementera PKCE i dag innebär att du är förberedd för den standarden när den formaliseras.
Det praktiska resultatet syns i statistiken. Auktoriseringskodinterception representerade historiskt en av de vanligaste attackvektorerna mot OAuth-implementationer, och PKCE eliminerar den kategorin helt. Från och med 2026 kräver leverantörer som Google, GitHub, Okta och Microsoft PKCE för alla nya appregistreringar som använder auktoriseringskodflödet.
Varför PKCE ersatte Implicit Flow
Det implicita flödet returnerade åtkomsttoken direkt i redirect-URL:ens fragment (#access_token=eyJ...). Det innebar tre allvarliga problem som länge var kända men ignorerades på grund av bekvämlighet. Problemens svårighetsgrad ökade i takt med att JavaScript-applikationer tog över allt mer av webbens auktoriseringslogik.
För det första loggades token ofta av webbservrar, proxies och reverse proxies som bevarade hela URL:en inklusive fragmentet i sina accessloggar. En angripare med tillgång till serverns loggfiler fick direkt tillgång till aktiva åtkomsttoken utan att behöva angripa kryptografin. För det andra exponerades token för JavaScript på sidan via window.location.hash, vilket öppnade för XSS-attacker. Varje JavaScript-injektion på sidan gav angriparen omedelbar tillgång till token. För det tredje gick det inte att verifiera att rätt klient tog emot token, vilket möjliggjorde token-stuffing-attacker.
Auktoriseringskodflödet med PKCE eliminerar dessa problem i grunden. Token utbyts aldrig i URL:en, utan alltid via en server-till-server POST-begäran från din backend till auktoriseringsserverns token-endpoint. Den kryptografiska bindningen med code_verifier/code_challenge säkerställer att bara den klient som initierade flödet kan slutföra det, oavsett om en angripare lyckas fånga upp auktoriseringskoden i transit.
Hur PKCE-bindningen fungerar matematiskt
PKCE bygger på pre-image-resistansen hos SHA-256. Flödet fungerar i fem steg. Din app genererar ett slumpmässigt code_verifier på 43-128 tecken. Den beräknar sedan code_challenge = BASE64URL(SHA256(code_verifier)). Auktoriseringsservern tar emot code_challenge och lagrar den kopplad till auktoriseringskoden. Vid token-utbytet skickar appen code_verifier i klartext. Servern beräknar SHA-256 av code_verifier och jämför med den lagrade code_challenge. En angripare som interceptade code_challenge i steg 2 kan inte bakåtberäkna code_verifier eftersom SHA-256 är en envägsfunktion utan praktisk inversmöjlighet.
Enligt RFC 9126 rekommenderas PKCE nu för alla OAuth-klienter, inte bara mobil- och SPA-applikationer. Konfidentiella klienter med klienthemlighet bör också använda PKCE som ett extra försvarslager, eftersom det skyddar mot scenarion där auktoriseringsserverns lagring av code_challenge inte angrips direkt men kodinterception sker i nätverket.
Förutsättningar
Innan du börjar behöver du dessa verktyg och kunskaper på plats:
| Krav | Version / detalj | Syfte i projektet |
|---|---|---|
| Node.js | 22.x LTS (minst 18.x) | Runtime med inbyggd crypto-modul |
| npm | 10.x | Pakethantering och skriptköring |
| Express | 4.21.x | HTTP-server och routing |
| express-session | 1.18.x | Sessionslagring server-side |
| axios | 1.7.x | HTTP-klient för token-endpoint |
| dotenv | 16.x | Miljövariabelhantering |
| OAuth 2.0-leverantör | Google, GitHub, Okta eller Keycloak | Auktoriseringsserver |
| Grundläggande Express-kunskap | Routing, middleware | Förståelse av kodbasen |
Du behöver också en registrerad applikation hos din valda OAuth-leverantör. Vid registreringen sätter du en godkänd redirect URI, t.ex. http://localhost:3000/auth/callback för lokal utveckling. Från leverantören får du ett Client ID och eventuellt ett Client Secret. Public clients som SPA:er och mobilappar registreras utan client_secret och förlitar sig helt på PKCE för säkerheten.
Kontrollera din Node.js-version innan du börjar:
node --version
# Förväntad output: v22.x.x (eller v20.x.x som minimum)
npm --version
# Förväntad output: 10.x.x
Steg 1-3: Projektsetup och installation
Skapa projektkatalogen och installera beroendena:
mkdir oauth2-pkce-demo && cd oauth2-pkce-demo
npm init -y
npm install express express-session axios dotenv
npm install --save-dev nodemon
Skapa filstrukturen som håller projektet välorganiserat:
oauth2-pkce-demo/
├── .env # Hemliga konfigurationsvärden (läggs aldrig i git)
├── .gitignore
├── package.json
└── src/
├── server.js # Express-server och grundkonfiguration
├── pkce.js # PKCE-nyckelgenerering
├── routes/
│ ├── auth.js # /auth/login, /auth/callback, /auth/logout
│ └── protected.js # Skyddade API-rutter
└── middleware/
└── requireAuth.js # Autentiserings- och token-validering
Skapa .env-filen med dina OAuth-uppgifter. Byt ut exempelvärdena mot riktiga värden från din leverantör:
# .env
SESSION_SECRET=byt-ut-detta-till-ett-langt-slumpmassigt-varde-minst-32-tecken
CLIENT_ID=din-client-id-har
CLIENT_SECRET=din-client-secret-har
# Redirect URI måste matcha exakt vad du registrerat hos leverantören
REDIRECT_URI=http://localhost:3000/auth/callback
# Google OAuth-slutpunkter (byt ut mot din leverantörs URL:er)
AUTHORIZATION_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth
TOKEN_ENDPOINT=https://oauth2.googleapis.com/token
USERINFO_ENDPOINT=https://openidconnect.googleapis.com/v1/userinfo
# Scopes: openid aktiverar OIDC-läget med id_token
SCOPES=openid email profile
PORT=3000
NODE_ENV=development
Lägg omedelbart till .env i .gitignore. Det är det vanligaste misstaget som leder till att API-nycklar exponeras i git-historiken:
printf ".env\nnode_modules/\n" > .gitignore
Uppdatera package.json med startskript och typ-fält:
{
"name": "oauth2-pkce-demo",
"version": "1.0.0",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"dependencies": {
"axios": "^1.7.0",
"dotenv": "^16.0.0",
"express": "^4.21.0",
"express-session": "^1.18.0"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
Steg 4-5: Serverkonfiguration och sessionshantering
Skapa src/server.js. Det är startpunkten som kopplar ihop Express, sessioner och rutter. Varje sessionskonfigurationsparameter har en direkt säkerhetspåverkan:
// src/server.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const authRoutes = require('./routes/auth');
const protectedRoutes = require('./routes/protected');
const app = express();
const PORT = process.env.PORT || 3000;
// Lita på proxy om du kör bakom Nginx eller Cloudflare i produktion
app.set('trust proxy', 1);
// Sessionskonfiguration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // Kräv HTTPS i produktion
httpOnly: true, // Förhindrar åtkomst via JavaScript (skyddar mot XSS)
sameSite: 'lax', // Tillåter redirect-cookies men blockerar cross-site POST
maxAge: 1000 * 60 * 60 * 24 // 24 timmar
}
}));
// Rutter
app.use('/auth', authRoutes);
app.use('/api', protectedRoutes);
// Startsida
app.get('/', (req, res) => {
if (req.session.user) {
return res.send(`
Inloggad som ${escapeHtml(req.session.user.email)}
Visa profil |
Logga ut
`);
}
res.send('OAuth 2.0 + PKCE Demo
Logga in');
});
// Enkel HTML-escape för att förhindra XSS i svar
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
app.listen(PORT, () => {
console.log(`Server körs på http://localhost:${PORT}`);
});
Sessionsinställningen httpOnly: true är kritisk. Den hindrar JavaScript från att läsa sessionscookien, vilket eliminerar den vanligaste XSS-attackvektorn mot sessionstoken. sameSite: 'lax' tillåter att cookien skickas med navigationsredirects, vilket OAuth-flödet kräver, men blockerar cookien vid cross-site POST-förfrågningar. Lägg märke till att vi kör med secure: false lokalt under utveckling men sätter secure: true i produktion via miljövariabeln.
Observera även escapeHtml-funktionen i startsidan. Att skriva ut req.session.user.email direkt i HTML utan escape skapar en stored XSS-sårbarhet om ett konto registrerats med ett epostnamn som innehåller HTML-tecken. Det är ett litet tillägg med stor säkerhetspåverkan.
Steg 6: PKCE-nyckelgenerering
Skapa src/pkce.js med den kryptografiska logiken. Node.js inbyggda crypto-modul hanterar allt. Inga externa beroenden behövs för den här kärndelen:
// src/pkce.js
const crypto = require('crypto');
// Genererar ett kryptografiskt säkert code_verifier.
// RFC 7636 kräver 43-128 tecken från alfabetet [A-Z a-z 0-9 - . _ ~].
// 32 slumpmässiga bytes base64url-kodade ger exakt 43 tecken utan padding.
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
// Beräknar code_challenge från code_verifier med S256-metoden.
// S256 = BASE64URL(SHA256(ASCII(code_verifier)))
// Auktoriseringsservern lagrar code_challenge och verifierar
// att SHA256(verifier) == challenge vid tokenutbytet.
function generateCodeChallenge(codeVerifier) {
const hash = crypto.createHash('sha256').update(codeVerifier).digest();
return hash.toString('base64url');
}
// Genererar ett kryptografiskt slumpmässigt state-värde.
// State-parametern skyddar mot CSRF-attacker i OAuth-flödet.
function generateState() {
return crypto.randomBytes(16).toString('hex');
}
module.exports = { generateCodeVerifier, generateCodeChallenge, generateState };
Varför base64url och inte standard base64? Standard base64 använder tecknen +, / och = som måste URL-encodas. Base64url ersätter dem med -, _ och utelämnar padding, vilket ger en sträng som är direkt säker att inkludera i URL-parametrar utan ytterligare encoding. RFC 7636 specificerar explicit base64url-encoding för både code_verifier och code_challenge.
Undvik 'plain'-metoden för code_challenge. Den skickar code_verifier i klartext som code_challenge, vilket eliminerar PKCE:s skydd helt. En angripare som fångade auktoriseringsförfrågan har då allt som behövs för att byta koden mot tokens. RFC 9700 rekommenderar att auktoriseringsservrar avvisar plain-metoden och bara accepterar S256.
Steg 7-8: Auktoriseringsflöde och redirect
Skapa src/routes/auth.js. Den hanterar tre slutpunkter: /login som startar PKCE-flödet, /callback som tar emot auktoriseringskoden från leverantören, och /logout som avslutar sessionen:
// src/routes/auth.js
const express = require('express');
const axios = require('axios');
const { generateCodeVerifier, generateCodeChallenge, generateState } = require('../pkce');
const router = express.Router();
// Steg 7: Starta OAuth 2.0 + PKCE-flödet
router.get('/login', (req, res) => {
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const state = generateState();
// Spara code_verifier och state i sessionen INNAN redirect.
// code_verifier skickas ALDRIG till auktoriseringsservern.
req.session.codeVerifier = codeVerifier;
req.session.oauthState = state;
const params = new URLSearchParams({
client_id: process.env.CLIENT_ID,
redirect_uri: process.env.REDIRECT_URI,
response_type: 'code',
scope: process.env.SCOPES,
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
const authUrl = `${process.env.AUTHORIZATION_ENDPOINT}?${params}`;
res.redirect(authUrl);
});
// Steg 8: Hantera callback från auktoriseringsservern
router.get('/callback', async (req, res) => {
const { code, state, error, error_description } = req.query;
// Kontrollera om auktoriseringsservern returnerade ett fel
if (error) {
console.error('OAuth-fel:', error, error_description);
return res.status(400).send(`Auktoriseringsfel: ${error}`);
}
// Verifiera state-parametern för att förhindra CSRF-attacker
if (!state || state !== req.session.oauthState) {
return res.status(403).send('Ogiltig state-parameter. Möjlig CSRF-attack.');
}
// Hämta code_verifier från sessionen
const codeVerifier = req.session.codeVerifier;
if (!codeVerifier) {
return res.status(400).send('Saknad code_verifier. Börja om inloggningsprocessen.');
}
try {
// Byt auktoriseringskoden mot access_token och refresh_token
const tokenResponse = await axios.post(
process.env.TOKEN_ENDPOINT,
new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET, // Utelämna för public clients
code: code,
redirect_uri: process.env.REDIRECT_URI,
code_verifier: codeVerifier // PKCE: verifier som auktoriseringsservern kontrollerar
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
const { access_token, refresh_token, expires_in } = tokenResponse.data;
// Rensa PKCE-data från sessionen direkt efter lyckad utbyte
delete req.session.codeVerifier;
delete req.session.oauthState;
// Hämta användarinformation med access_token
const userResponse = await axios.get(process.env.USERINFO_ENDPOINT, {
headers: { Authorization: `Bearer ${access_token}` }
});
// Spara autentiseringsdata i sessionen
req.session.user = userResponse.data;
req.session.accessToken = access_token;
req.session.refreshToken = refresh_token || null;
req.session.tokenExpiry = Date.now() + (expires_in * 1000);
res.redirect('/');
} catch (err) {
const errData = err.response?.data;
console.error('Token-utbytesfel:', errData || err.message);
res.status(500).send('Autentisering misslyckades. Försök igen.');
}
});
// Steg 12: Logga ut och förstör sessionen
router.get('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).send('Utloggning misslyckades.');
}
res.clearCookie('connect.sid');
res.redirect('/');
});
});
module.exports = router;
State-valideringen på raderna 35-38 är obligatorisk. Utan den kan en angripare lura din app att acceptera en auktoriseringskod från en annan användares inloggningssession. Det klassiska OAuth CSRF-angreppet fungerar så: angriparen initierar ett inloggningsflöde, avbryter det och ger offret en länk till callback-URL:en med angriparens kod. Offrets webbläsare skickar offrets session-cookie till din callback, och om du inte kontrollerar state loggas offret in som angriparen. Varje nytt inloggningsförsök genererar ett nytt, unikt state-värde som bara gäller för den specifika sessionen.
Steg 9: Autentiseringsmiddleware och skyddade rutter
Skapa middleware som kontrollerar både autentisering och token-giltighet, med automatisk förnyelse via refresh_token:
// src/middleware/requireAuth.js
const axios = require('axios');
async function requireAuth(req, res, next) {
// Kontrollera att användaren är inloggad
if (!req.session.user || !req.session.accessToken) {
return res.status(401).json({
error: 'Inte autentiserad.',
loginUrl: '/auth/login'
});
}
// Förnya token 60 sekunder innan den går ut för att undvika race conditions
const bufferMs = 60 * 1000;
const tokenExpiresSoon = req.session.tokenExpiry &&
Date.now() > req.session.tokenExpiry - bufferMs;
if (tokenExpiresSoon) {
if (!req.session.refreshToken) {
req.session.destroy(() => {});
return res.status(401).json({
error: 'Session utgången.',
loginUrl: '/auth/login'
});
}
try {
const refreshResponse = await axios.post(
process.env.TOKEN_ENDPOINT,
new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
refresh_token: req.session.refreshToken
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
const { access_token, refresh_token, expires_in } = refreshResponse.data;
req.session.accessToken = access_token;
// Vissa leverantörer roterar refresh_token; behåll gamla om ny saknas
req.session.refreshToken = refresh_token || req.session.refreshToken;
req.session.tokenExpiry = Date.now() + (expires_in * 1000);
} catch (err) {
req.session.destroy(() => {});
return res.status(401).json({
error: 'Token-förnyelse misslyckades.',
loginUrl: '/auth/login'
});
}
}
next();
}
module.exports = requireAuth;
Skapa de skyddade rutterna i src/routes/protected.js:
// src/routes/protected.js
const express = require('express');
const requireAuth = require('../middleware/requireAuth');
const router = express.Router();
// Tillämpa autentiseringsmiddleware på alla /api-rutter
router.use(requireAuth);
router.get('/profile', (req, res) => {
res.json({
message: 'Profildata hämtad',
user: {
sub: req.session.user.sub,
name: req.session.user.name,
email: req.session.user.email
},
tokenExpiry: new Date(req.session.tokenExpiry).toISOString()
});
});
router.get('/dashboard', (req, res) => {
res.json({
message: `Välkommen tillbaka, ${req.session.user.name || req.session.user.email}`,
scopes: process.env.SCOPES.split(' ')
});
});
module.exports = router;
Observera att vi returnerar ett minimalt subset av användardata från /api/profile. Vi undviker att exponera hela req.session.user-objektet direkt eftersom det kan innehålla mer information från leverantören än vad frontend-koden behöver. Det minimerar datamängden som exponeras vid eventuell IDOR-sårbarhet. Läs mer om dessa mönster i vår guide om OWASP Top 10 i Node.js.
Steg 10-11: Testning av hela OAuth-flödet
Starta servern i utvecklingsläge:
npm run dev
# Förväntad terminal-output:
# [nodemon] starting `node src/server.js`
# Server körs på http://localhost:3000
Öppna webbläsaren och navigera till http://localhost:3000. Klicka på “Logga in”. Du omdirigeras till auktoriseringsservern. Granska URL-parametrarna i adressfältet. Du ska se code_challenge, code_challenge_method=S256 och state. Du ser aldrig code_verifier i URL:en, den existerar bara i din server-session.
Efter godkänd inloggning omdirigeras du till /auth/callback?code=...&state=.... Servern verifierar state, hämtar code_verifier från sessionen och byter auktoriseringskoden mot tokens. Du hamnar tillbaka på startsidan inloggad.
Testa det skyddade API:et med curl. Du behöver en aktiv session-cookie från webbläsarens developer tools, eller spara den under inloggning:
# Spara session-cookie under inloggning
curl -c /tmp/oauth-cookies.txt -L http://localhost:3000/auth/login
# Testa skyddad rutt med sparad cookie
curl -b /tmp/oauth-cookies.txt http://localhost:3000/api/profile
# Förväntad JSON-respons:
{
"message": "Profildata hämtad",
"user": {
"sub": "110169484474386276334",
"name": "Anna Lindqvist",
"email": "[email protected]"
},
"tokenExpiry": "2026-06-18T16:30:00.000Z"
}
# Test utan cookie: ska returnera 401
curl http://localhost:3000/api/profile
# {"error":"Inte autentiserad.","loginUrl":"/auth/login"}
Observera att curl-flödet kräver att du manuellt godkänner inloggning i webbläsaren eftersom auktoriseringsservern presenterar sitt eget formulär. I automatiserade integrationstester används vanligtvis en test-OAuth-server som oidc-provider eller en mock-implementation.
Komplett projektöversikt: hela dataflödet
Här är en fullständig bild av hur data flödar genom systemet från första click till aktiv session:
| Steg | Aktör | Åtgärd | Data som skickas | Data som sparas |
|---|---|---|---|---|
| 1 | Browser | GET /auth/login | Session-cookie | Ingenting |
| 2 | Node.js-server | Genererar verifier, challenge och state | Ingenting externt | code_verifier, oauthState i session |
| 3 | Node.js-server | Redirect till auktoriseringsserver | client_id, code_challenge, S256, state, scope | Ingenting nytt |
| 4 | Användare | Loggar in och godkänner | Inloggningsuppgifter till auktoriseringsservern | Hos auktoriseringsservern |
| 5 | Auktoriseringsserver | Redirect till /auth/callback | code (engångsbruk), state | Ingenting på klienten |
| 6 | Node.js-server | Verifierar state, hämtar code_verifier ur session | Intern sessionslookup | Ingenting nytt |
| 7 | Node.js-server | POST till token_endpoint | code, code_verifier, client_id, redirect_uri | Ingenting ännu |
| 8 | Auktoriseringsserver | Verifierar SHA256(verifier) == challenge | Returnerar access_token, refresh_token | Hos auktoriseringsservern |
| 9 | Node.js-server | GET userinfo med access_token | Bearer-token i header | user, accessToken, refreshToken, tokenExpiry i session |
| 10 | Node.js-server | Rensar PKCE-data, redirect till / | Session-cookie till browser | code_verifier och oauthState raderas |
Vanliga misstag som kostar tid
Dessa sex misstag uppstår konsekvent i OAuth 2.0 + PKCE-implementationer och leder till antingen säkerhetshål eller brutna flöden.
Misstag 1: Skickar code_verifier i auktoriseringsförfrågan
Det absolut vanligaste misstaget bland utvecklare som läser PKCE för första gången: skicka code_verifier istället för code_challenge i steg 3. Auktoriseringsservern tar emot en parameter den inte förstår, flödet misslyckas med invalid_request, och felmeddelandena ger sällan tydlig vägledning. Regeln är enkel: code_challenge går till auktoriseringsservern i steg 3. code_verifier går till token-endpoint i steg 7. Aldrig tvärtom.
Misstag 2: Återanvänder code_verifier mellan sessioner
RFC 7636 kräver att code_verifier är ny för varje auktoriseringsförfrågan. Om du lagrar code_verifier i en global variabel istället för i sessionen kan ett race condition uppstå: två parallella inloggningar blandar ihop varandras verifiers. Det leder till sporadiska invalid_grant-fel som är nästintill omöjliga att reproducera i lokal utveckling men dyker upp regelbundet under hög trafik i produktion.
Misstag 3: Hoppar över state-valideringen
State-parametern är inte valfri. Utan state-validering i callback-hanteraren exponerar du appen för OAuth CSRF-attacker. OWASP dokumenterar detta angreppsmönster som ett av de vanligaste OAuth-säkerhetsproblemen. Resultatet av en lyckad attack är att angriparen loggas in som ett offer i din app, eller att offrets konto kopplas till angriparens externa identitet.
Misstag 4: Lagrar access_token i localStorage
Många tutorials visar att man sparar access_token i webbläsarens localStorage för att sedan skicka den med API-anrop via JavaScript. Det är ett allvarligt säkerhetsfel. localStorage är åtkomlig från all JavaScript på sidan, vilket innebär att en enda XSS-sårbarhet ger angriparen tillgång till token. Lagra alltid token server-side i en HTTP-only session-cookie, som vi gör i den här implementationen. Se vår guide om Node.js sessionshantering för detaljer om säker cookiekonfiguration.
Misstag 5: Använder ‘plain’ som code_challenge_method
Plain-metoden skickar code_verifier som code_challenge i klartext. Det innebär att auktoriseringsservern tar emot den faktiska verifieraren redan i steg 3, och en angripare som fångar upp auktoriseringsförfrågan har allt som behövs för att byta koden mot tokens. Plain-metoden existerar bara för bakåtkompatibilitet med äldre system. Moderna auktoriseringsservrar ska konfigureras att avvisa plain och bara acceptera S256.
Misstag 6: Glömmer att rensa PKCE-data efter tokenutbytet
code_verifier och oauthState ska raderas ur sessionen direkt efter att tokenutbytet lyckats. Om du behåller dem uppstår förvirring om användaren startar ett nytt inloggningsflöde utan att logga ut, och du riskerar att sessionen innehåller stale PKCE-data från tidigare flöden. I vår implementation hanteras detta med delete req.session.codeVerifier och delete req.session.oauthState direkt efter lyckad token-hämtning.
Felsökningsguide: 8 vanliga OAuth-fel
Dessa åtta fel dyker upp regelbundet i PKCE-implementationer. Här är symptomen, orsakerna och lösningarna:
| Felmeddelande | HTTP-kod | Vanligaste orsak | Lösning |
|---|---|---|---|
invalid_grant | 400 | code_verifier matchar inte challenge, eller koden har redan använts | Verifiera att SHA256(verifier) ger rätt challenge. Auktoriseringskoder är engångsbruk. |
invalid_request | 400 | Redirect URI ej registrerad, eller saknad PKCE-parameter | Kontrollera att redirect_uri matchar exakt vad du registrerat hos leverantören, tecken för tecken. |
invalid_client | 401 | Fel client_id eller client_secret | Kontrollera .env-värdena. Koda aldrig in dem direkt i källkoden. |
| State mismatch (din 403) | 403 | Sessionen gick ut mellan login och callback, eller cookies blockeras | Kontrollera att sameSite=’lax’ är satt och att sessionen lever länge nog för inloggningsflödet. |
| CORS-fel vid token-endpoint | Webbläsarfel | Token-request görs direkt från frontend-JavaScript | Token-request MÅSTE göras server-side. Flytta axios.post-anropet till Node.js-backend. |
access_denied | 400 | Användaren nekade åtkomst, eller scope saknas i leverantörens konfiguration | Kontrollera att begärda scopes är aktiverade i leverantörens app-konfiguration. |
| Cannot read properties of undefined (codeVerifier) | 500 | Session initialiserades inte, eller express-session saknas | Verifiera att app.use(session(…)) körs INNAN routing-middleware i server.js. |
redirect_uri_mismatch | 400 | URI i koden matchar inte registrerad URI hos leverantören | URI:n måste matcha tecken för tecken inklusive avslutande snedstreck och PORT-nummer. |
För djupare felsökning, aktivera temporär logging av OAuth-parametrarna. Logga aldrig code_verifier, access_token eller refresh_token i produktionsloggar:
// Felsökningslogging: aktivera bara i development
if (process.env.NODE_ENV === 'development') {
console.log('OAuth-parametrar:', {
authorization_endpoint: process.env.AUTHORIZATION_ENDPOINT,
client_id: process.env.CLIENT_ID,
redirect_uri: process.env.REDIRECT_URI,
scope: process.env.SCOPES,
code_challenge_method: 'S256',
// Logga ALDRIG code_verifier, access_token eller refresh_token
code_challenge_preview: codeChallenge.substring(0, 10) + '...'
});
}
Avancerade tekniker och produktionsinställningar
Grundimplementationen fungerar för prototyping och inlärning. Produktionsmiljöer kräver ytterligare härdning på fyra nivåer:
Persistent sessionslagring med Redis
Minnesbaserad sessionslagring försvinner vid serveromstart. För produktionsmiljöer med flera instanser krävs en delad sessionslagring som Redis. Installera connect-redis:
npm install connect-redis redis
// src/server.js - produktionskonfiguration
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect().catch(console.error);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // Kräv HTTPS i produktion
httpOnly: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 8 // 8 timmar
}
}));
OpenID Connect ID-token-validering
Om auktoriseringsservern returnerar ett id_token (ett JWT med användarinformation via OpenID Connect), validera det innan du litar på innehållet. Biblioteket jose hanterar JWKS-baserad JWT-validering:
npm install jose
const { createRemoteJWKSet, jwtVerify } = require('jose');
const JWKS = createRemoteJWKSet(
new URL('https://accounts.google.com/.well-known/openid-configuration')
);
async function validateIdToken(id_token) {
const { payload } = await jwtVerify(id_token, JWKS, {
issuer: 'https://accounts.google.com',
audience: process.env.CLIENT_ID
});
// Kontrollera nonce om du använde en i auktoriseringsförfrågan
return payload;
}
Läs mer om JWT-validering i vår guide om JWT-autentisering i Node.js.
PKCE-flödet utan client_secret för public clients
Single-page applikationer och mobilappar kan inte säkra ett client_secret; källkoden är alltid tillgänglig för användaren. För dessa public clients utelämnas client_secret helt från token-förfrågan. PKCE ersätter behovet av client_secret eftersom code_verifier bevisar att rätt klient, den som skapade code_challenge, slutförde flödet. Registrera appen som “public client” hos leverantören för att tillåta tokenutbyte utan secret.
Hastighetsbegränsning på auth-endpoints
OAuth-endpoints är frekventa mål för scanning och missbruk. Lägg till rate limiting på /auth/login och /auth/callback. Vår guide om rate limiting i Node.js visar hur du konfigurerar express-rate-limit för just auth-flöden. Kombinera med tvåfaktorsautentisering i Node.js för ett komplett autentiseringslager. Se även Express.js officiella säkerhetsguide för ytterligare produktionshärdning med Helmet.js och Content Security Policy. CSRF-skydd i Node.js täcker de attacker som OAuth-flödets state-parameter förhindrar i autentiseringsflödet, men som kan dyka upp i andra delar av din app. Auth0:s dokumentation om auktoriseringskodflödet med PKCE ger en leverantörsneutral referensimplementation.
Relaterad läsning
Relaterade artiklar
- JWT-autentisering i Node.js: 10 steg – tokenbaserad autentisering som kompletterar OAuth 2.0
- CSRF-skydd i Node.js: 12 steg – de attacker som OAuth state-parametern förhindrar i auth-flödet
- Node.js sessionshantering: 11 steg – djupgående sessionsarkitektur och säker cookiekonfiguration
- Tvåfaktorsautentisering i Node.js: 11 steg – lägg till TOTP/2FA ovanpå OAuth-inloggning
- Rate Limiting i Node.js: 12 steg – skydda auth-endpoints mot brute force och scanning
- OWASP Top 10 i Node.js: 12 steg – bredare säkerhetskontext inklusive broken access control och injection
Vanliga frågor om OAuth 2.0 och PKCE
Behöver jag PKCE om jag redan har en client_secret?
Ja. RFC 9700 och OAuth 2.0 Security Best Current Practice rekommenderar PKCE för alla klienttyper inklusive konfidentiella klienter med client_secret. PKCE och client_secret är kompletterande, inte alternativa, säkerhetsmekanismer. Client_secret autentiserar klienten mot servern. PKCE binder auktoriseringskoden till den specifika session som initierade flödet. Båda mekanismerna bidrar med distinkt skydd mot olika angreppsscenarier.
Vad händer om auktoriseringsservern inte stöder PKCE?
Auktoriseringsservern returnerar invalid_request om den inte känner igen PKCE-parametrarna. I det läget kan du använda standard auktoriseringskodflödet med client_secret, men bör planera migration till en PKCE-kompatibel leverantör. Alla moderna OAuth-implementationer, Google, GitHub, Okta, Keycloak och Auth0, stöder PKCE sedan flera år tillbaka.
Hur lång ska code_verifier vara?
RFC 7636 kräver 43-128 tecken. Kortare verifiers ger svagare entropi. Längre ger onödigt overhead. En verifier genererad från 32 slumpmässiga bytes base64url-kodad ger exakt 43 tecken med tillräcklig entropi för alla praktiska säkerhetsbehov. Det är vad implementationen ovan genererar med crypto.randomBytes(32).toString('base64url').
Kan jag använda PKCE med GitHub OAuth?
GitHub stöder PKCE för OAuth Apps och GitHub Apps sedan 2023. Lägg till code_challenge och code_challenge_method=S256 i auktoriseringsförfrågan till https://github.com/login/oauth/authorize och skicka code_verifier vid tokenutbytet till https://github.com/login/oauth/access_token. GitHub-dokumentationen markerar PKCE som rekommenderat för alla nya app-integrationer.
Vad är skillnaden mellan OAuth 2.0 och OpenID Connect?
OAuth 2.0 är ett auktoriseringsramverk: det handlar om att ge din app tillstånd att göra saker å en användares vägnar. OpenID Connect (OIDC) är ett identitetslager ovanpå OAuth 2.0 som lägger till autentisering. OIDC returnerar ett id_token, ett JWT med verifierbar användarinformation, utöver access_token. Scope-värdet openid aktiverar OIDC-läget. I implementationen ovan aktiverar du OIDC automatiskt genom att inkludera openid i SCOPES-miljövariabeln.
Hur hanterar jag logout korrekt i OAuth?
Komplett OAuth-logout kräver tre steg. Steg 1: förstör den lokala Express-sessionen med req.session.destroy() och rensa session-cookien. Steg 2: omdirigera användaren till auktoriseringsserverns logout-endpoint för att ogiltigförklara access_token och refresh_token hos leverantören. Steg 3: verifiera att token faktiskt är ogiltigförklarade genom ett test-anrop mot userinfo-endpoint. Utan steg 2 förblir token giltiga hos auktoriseringsservern trots att din lokala session är raderad.
Vad händer om code_verifier läcker ut?
Om en angripare fick tillgång till code_verifier ur din session kunde de, i kombination med en interceptad auktoriseringskod, byta koden mot tokens. Det är extremt svårt i praktiken eftersom det kräver samtidig åtkomst till sessionslagringen och nätverkstrafiken. Försvarslinjerna: använd Redis-baserade sessioner (inte minnessessioner) med begränsad TTL, säkerställ httpOnly på sessionscookien, och radera code_verifier ur sessionen omedelbart efter lyckad token-utbyte, vilket implementationen ovan gör.
Registrera din app hos Google OAuth 2.0
Google är den vanligaste OAuth-leverantören för webbutvecklare i Sverige och Norden. Registreringen tar under tio minuter och ger dig de client_id och authorization endpoint-URL:er som implementationen ovan behöver. Här är den exakta proceduren för att komma igång med Google Identity Platform.
Navigera till Google Cloud Console och skapa ett nytt projekt om du inte redan har ett. Under menyn “APIs & Services” väljer du “OAuth consent screen”. Välj “External” som användartyp om du vill att externa Google-konton ska kunna logga in. Fyll i applikationsnamnet, din e-postadress och eventuellt en logotyp. Under “Scopes” lägger du till de tre standardscopes som implementationen behöver: openid, email och profile. Dessa ger tillgång till användarens namn, e-postadress och profilbild utan att begära tillgång till Drive, Gmail eller andra känsliga resurser.
Gå sedan till “Credentials” och klicka “Create Credentials” följt av “OAuth 2.0 Client IDs”. Välj applikationstypen “Web application”. Under “Authorized redirect URIs” lägger du till din callback-URL. Beroende på miljö kan du behöva lägga till flera URI:er:
| Miljö | Redirect URI att registrera | Kommentar |
|---|---|---|
| Lokal utveckling | http://localhost:3000/auth/callback | HTTP tillåtet för localhost |
| Staging | https://staging.din-app.se/auth/callback | HTTPS krävs för externa domäner |
| Produktion | https://din-app.se/auth/callback | HTTPS krävs, inga IP-adresser |
Google skapar ett Client ID på formatet xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com och ett Client Secret. Kopiera båda direkt till din .env-fil. Google-specifika endpoint-URL:er är:
# Google-specifika OAuth-endpoints för .env
AUTHORIZATION_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth
TOKEN_ENDPOINT=https://oauth2.googleapis.com/token
USERINFO_ENDPOINT=https://openidconnect.googleapis.com/v1/userinfo
# Alternativt via Google Discovery Document:
# https://accounts.google.com/.well-known/openid-configuration
En viktig detalj: Google kräver att din app befinner sig i “Testing”-läget under utveckling. I testläget kan maximalt 100 testanvändare logga in. För att öppna appen för alla Google-användare måste du genomgå en verifieringsprocess som kan ta 1-3 veckor för nya appar. Planera för det om du ska lansera en produkt med Google-inloggning.
Verifiera att kopplingen fungerar genom att kontrollera att din app genererar en korrekt auktoriserings-URL. En giltig Google OAuth 2.0 + PKCE-URL ser ut ungefär så här:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=123456789-abc.apps.googleusercontent.com&
redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback&
response_type=code&
scope=openid+email+profile&
state=a8f3c2d9e1b4f7a0&
code_challenge=somerandombase64urlstring43characterslongg&
code_challenge_method=S256
Notera att code_verifier inte syns i URL:en alls. Den lagrades i sessionen när din server genererade PKCE-parametrarna. Det är precis den egenskap som gör PKCE säkert: auktoriseringsservern ser aldrig verifieraren, bara challenge-hashen som den sedan jämför mot verifieraren vid tokenutbytet.
Keycloak som självhostad OAuth-server
Organisationer med strikta dataskyddskrav, till exempel enligt GDPR eller den svenska Dataskyddsförordningen, kan behöva hålla all autentiseringsdata inom EU. Keycloak är en öppen källkod-identitetsserver som du kan driftsätta på din egen infrastruktur och som har fullt PKCE-stöd.
En lokal Keycloak-instans för testning körs enkelt via Docker:
# Starta Keycloak lokalt med Docker
docker run -d \
--name keycloak \
-p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-dev
# Keycloak Admin Console: http://localhost:8080/admin
I Keycloak Admin Console skapar du ett nytt “Realm” (t.ex. “demo”), lägger till en klient med typen “OpenID Connect” och aktiverar “Standard flow” (auktoriseringskodflödet). I klientens “Advanced”-inställningar aktiverar du “Proof Key for Code Exchange Code Challenge Method” och väljer S256. Lägg till din redirect URI i “Valid redirect URIs”-fältet.
Keycloak-endpoints för en lokal installation och ett realm som heter “demo” ser ut så här:
# Keycloak-specifika endpoints för .env
AUTHORIZATION_ENDPOINT=http://localhost:8080/realms/demo/protocol/openid-connect/auth
TOKEN_ENDPOINT=http://localhost:8080/realms/demo/protocol/openid-connect/token
USERINFO_ENDPOINT=http://localhost:8080/realms/demo/protocol/openid-connect/userinfo
# Keycloak Discovery Document (alla endpoints automatiskt):
# http://localhost:8080/realms/demo/.well-known/openid-configuration
Fördelen med Keycloak är full kontroll: du bestämmer vilka attribut som ingår i tokens, kan koppla Keycloak till din befintliga LDAP/Active Directory-katalog, och all autentiseringsdata stannar på din egna infrastruktur. Det gör Keycloak till ett populärt val för svenska myndigheter, banker och hälsovårdsorganisationer som måste uppfylla specifika dataplaceringskrav.
Nackdelen är driftkostnaden. Keycloak kräver minst 512 MB RAM per instans och ett relationsdatabasbackend för produktionsdrift. Jämfört med att använda Google eller GitHub OAuth, där infrastrukturen sköts av leverantören, är det en betydande driftskostnad att väga mot dataskyddsvinsten.
Säkerhetshärdning av OAuth-implementationen
En fungerande OAuth 2.0 + PKCE-implementation är ett bra fundament, men produktionsmiljöer behöver ytterligare härdning utöver vad som visats i kärn-implementationen. Dessa tre åtgärder är de mest impactfulla:
Token-rotation och säker lagring
Refresh_token är lika känsliga som lösenord och ska behandlas därefter. I vår implementation lagras de i Express-sessionen, som i sin tur lagras server-side (antingen i minnet eller Redis). Det är korrekt. Men om du av någon anledning behöver lagra refresh_token i en databas, kryptera dem med AES-256 innan lagring. Vår guide om AES-256-kryptering i Node.js visar hur du gör det.
Aktivera refresh_token-rotation hos din leverantör om möjligt. Token-rotation innebär att varje gång en refresh_token används för att hämta ett nytt access_token, utfärdas också ett nytt refresh_token. Det gamla ogiltigförklaras. Det begränsar skadan om ett refresh_token läcker: angriparen kan bara använda det en gång innan det uppdateras.
CORS-konfiguration för API-endpoints
Dina /api/*-endpoints behöver CORS-headers om de anropas från ett annat ursprung, t.ex. från en SPA på en annan port eller domän. Installera cors-paketet och konfigurera det restriktivt:
npm install cors
// src/server.js
const cors = require('cors');
const allowedOrigins = [
'http://localhost:3000',
'https://din-app.se'
];
app.use('/api', cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Inte tillåtet av CORS'));
}
},
credentials: true, // Krävs för att skicka session-cookies cross-origin
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Sätt aldrig origin: '*' (wildcard) på endpoints som kräver autentisering via cookies. Wildcard CORS och credentials: true är en ogiltig kombination som webbläsare avvisar, och med goda skäl: det skulle öppna för cross-site request forgery på dina API-anrop.
Loggning och audit trail
För NIS2-kompatibla system och GDPR-efterlevnad behöver du logga autentiseringshändelser utan att logga känslig data. Logga händelserna inloggning, utloggning, misslyckad autentisering och token-förnyelse med tidsstämpel och användar-ID (sub-claimet från id_token, inte användarens epostadress). Under NIS2-direktivet, som implementerades i Sverige via Cybersäkerhetslagen 2025:1506 och trädde i kraft den 15 januari 2026, krävs att organisationer i 18 kritiska sektorer kan demonstrera loggning av autentiseringshändelser som en del av sin incidentberedskap.
// Audit-logging för autentiseringshändelser (logga inte tokens)
function auditLog(event, userId, metadata = {}) {
const entry = {
timestamp: new Date().toISOString(),
event: event,
userId: userId,
ip: metadata.ip,
userAgent: metadata.userAgent?.substring(0, 100)
};
console.log(JSON.stringify(entry));
// I produktion: skicka till din log-aggregator (ELK, Splunk, osv.)
}
// I auth-routern efter lyckad inloggning:
auditLog('oauth_login_success', req.session.user.sub, {
ip: req.ip,
userAgent: req.headers['user-agent']
});
// Vid misslyckad state-validering:
auditLog('oauth_csrf_attempt', 'unknown', {
ip: req.ip,
receivedState: state?.substring(0, 8) + '...'
});
OAuth 2.0 med PKCE är inte bara ett tekniskt protokoll, det är en grundläggande byggsten i varje modern webbapplikations säkerhetsarkitektur. Implementationen i den här guiden ger dig ett komplett, produktionsnära OAuth-flöde som uppfyller kraven i RFC 7636, RFC 9700, och de kommande OAuth 2.1-standarderna. Kombinerat med de relaterade implementationsguiderna för JWT, tvåfaktorsautentisering och sessionshantering har du fundamentet för ett säkert autentiseringssystem som håller 2026 och framåt.



