{"id":113,"date":"2026-06-18T16:47:17","date_gmt":"2026-06-18T16:47:17","guid":{"rendered":"https:\/\/shattered.io\/se\/2026\/06\/18\/oauth2-pkce-nodejs\/"},"modified":"2026-06-18T16:51:25","modified_gmt":"2026-06-18T16:51:25","slug":"oauth2-pkce-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/se\/oauth2-pkce-nodejs\/","title":{"rendered":"OAuth 2.0 med PKCE i Node.js: 12 steg, 30 min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">OAuth 2.0 med PKCE (Proof Key for Code Exchange) \u00e4r 2026 \u00e5rs standardmetod f\u00f6r s\u00e4ker auktorisering i webbapplikationer. Den gamla implicita fl\u00f6det f\u00f6rsvann ur <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" target=\"_blank\" rel=\"noopener noreferrer\">OAuth 2.0-specifikationen (RFC 6749)<\/a> efter att forskning visade att kodinterception-attacker kunde stj\u00e4la \u00e5tkomsttoken direkt fr\u00e5n redirect-URL:en. PKCE, definierat i <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 7636<\/a>, l\u00f6ser det problemet med ett kryptografiskt handslag som binder auktoriseringskoden till den klient som skapade den. I den h\u00e4r guiden bygger du ett komplett OAuth 2.0 + PKCE-fl\u00f6de i Node.js och Express fr\u00e5n grunden, steg f\u00f6r steg.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vad-ar-oauth-2-0-och-pkce\">Vad \u00e4r OAuth 2.0 och PKCE?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 \u00e4r ett auktoriseringsramverk som l\u00e5ter en applikation beg\u00e4ra begr\u00e4nsad \u00e5tkomst till en anv\u00e4ndares konto hos en tredjepartstj\u00e4nst, utan att applikationen ser anv\u00e4ndarens l\u00f6senord. Fl\u00f6det involverar fyra parter: <strong>resurs\u00e4garen<\/strong> (anv\u00e4ndaren), <strong>klienten<\/strong> (din Node.js-app), <strong>auktoriseringsservern<\/strong> (t.ex. Google, GitHub, Okta) och <strong>resursservern<\/strong> (det API som skyddar anv\u00e4ndarens data).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE l\u00e4gger till ett extra lager ovanp\u00e5 auktoriseringskodfl\u00f6det. Utan PKCE kan en angripare som lyckas f\u00e5nga upp din auktoriseringskod, till exempel via ett elakt program p\u00e5 samma enhet eller via ett \u00f6ppet redirect, byta ut koden mot en \u00e5tkomsttoken. Med PKCE \u00e4r koden v\u00e4rdel\u00f6s utan <strong>code_verifier<\/strong>, en slumpm\u00e4ssig str\u00e4ng som bara din app k\u00e4nner till och som aldrig skickas i klartext under auktoriseringssteget.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fl\u00f6destyp<\/th><th>S\u00e4kerhetsniv\u00e5<\/th><th>Status 2026<\/th><th>Klienttyp<\/th><\/tr><\/thead><tbody><tr><td>Implicit Flow<\/td><td>L\u00e5g (token i URL)<\/td><td>F\u00f6r\u00e5ldrat, borttaget i OAuth 2.1<\/td><td>Public clients<\/td><\/tr><tr><td>Authorization Code<\/td><td>Medel (utan PKCE)<\/td><td>Acceptabelt f\u00f6r konfidentiella klienter<\/td><td>Confidential clients<\/td><\/tr><tr><td>Authorization Code + PKCE<\/td><td>H\u00f6g<\/td><td>Rekommenderat f\u00f6r alla klienttyper<\/td><td>Alla<\/td><\/tr><tr><td>Client Credentials<\/td><td>H\u00f6g (maskin-till-maskin)<\/td><td>Aktivt<\/td><td>Confidential clients<\/td><\/tr><tr><td>Device Code<\/td><td>Medel<\/td><td>Aktivt f\u00f6r IoT och CLI-appar<\/td><td>Enhetsbegr\u00e4nsade klienter<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.1-utkastet, som kombinerar erfarenheterna fr\u00e5n RFC 6749, RFC 7636 och <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc9700\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9700 (OAuth 2.0 Security Best Current Practice)<\/a>, kr\u00e4ver PKCE f\u00f6r alla auktoriseringskodfl\u00f6den och tar bort den implicita fl\u00f6det helt. Att implementera PKCE i dag inneb\u00e4r att du \u00e4r f\u00f6rberedd f\u00f6r den standarden n\u00e4r den formaliseras.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Det praktiska resultatet syns i statistiken. Auktoriseringskodinterception representerade historiskt en av de vanligaste attackvektorerna mot OAuth-implementationer, och PKCE eliminerar den kategorin helt. Fr\u00e5n och med 2026 kr\u00e4ver leverant\u00f6rer som Google, GitHub, Okta och Microsoft PKCE f\u00f6r alla nya appregistreringar som anv\u00e4nder auktoriseringskodfl\u00f6det.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"varfor-pkce-ersatte-implicit-flow\">Varf\u00f6r PKCE ersatte Implicit Flow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Det implicita fl\u00f6det returnerade \u00e5tkomsttoken direkt i redirect-URL:ens fragment (<code>#access_token=eyJ...<\/code>). Det innebar tre allvarliga problem som l\u00e4nge var k\u00e4nda men ignorerades p\u00e5 grund av bekv\u00e4mlighet. Problemens sv\u00e5righetsgrad \u00f6kade i takt med att JavaScript-applikationer tog \u00f6ver allt mer av webbens auktoriseringslogik.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00f6r det f\u00f6rsta loggades token ofta av webbservrar, proxies och reverse proxies som bevarade hela URL:en inklusive fragmentet i sina accessloggar. En angripare med tillg\u00e5ng till serverns loggfiler fick direkt tillg\u00e5ng till aktiva \u00e5tkomsttoken utan att beh\u00f6va angripa kryptografin. F\u00f6r det andra exponerades token f\u00f6r JavaScript p\u00e5 sidan via <code>window.location.hash<\/code>, vilket \u00f6ppnade f\u00f6r XSS-attacker. Varje JavaScript-injektion p\u00e5 sidan gav angriparen omedelbar tillg\u00e5ng till token. F\u00f6r det tredje gick det inte att verifiera att r\u00e4tt klient tog emot token, vilket m\u00f6jliggjorde token-stuffing-attacker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auktoriseringskodfl\u00f6det med PKCE eliminerar dessa problem i grunden. Token utbyts aldrig i URL:en, utan alltid via en server-till-server POST-beg\u00e4ran fr\u00e5n din backend till auktoriseringsserverns token-endpoint. Den kryptografiska bindningen med code_verifier\/code_challenge s\u00e4kerst\u00e4ller att bara den klient som initierade fl\u00f6det kan slutf\u00f6ra det, oavsett om en angripare lyckas f\u00e5nga upp auktoriseringskoden i transit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hur-pkce-bindningen-fungerar-matematiskt\">Hur PKCE-bindningen fungerar matematiskt<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE bygger p\u00e5 pre-image-resistansen hos SHA-256. Fl\u00f6det fungerar i fem steg. Din app genererar ett slumpm\u00e4ssigt <code>code_verifier<\/code> p\u00e5 43-128 tecken. Den ber\u00e4knar sedan <code>code_challenge = BASE64URL(SHA256(code_verifier))<\/code>. Auktoriseringsservern tar emot code_challenge och lagrar den kopplad till auktoriseringskoden. Vid token-utbytet skickar appen code_verifier i klartext. Servern ber\u00e4knar SHA-256 av code_verifier och j\u00e4mf\u00f6r med den lagrade code_challenge. En angripare som interceptade code_challenge i steg 2 kan inte bak\u00e5tber\u00e4kna code_verifier eftersom SHA-256 \u00e4r en env\u00e4gsfunktion utan praktisk inversm\u00f6jlighet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Enligt RFC 9126 rekommenderas PKCE nu f\u00f6r <strong>alla OAuth-klienter<\/strong>, inte bara mobil- och SPA-applikationer. Konfidentiella klienter med klienthemlighet b\u00f6r ocks\u00e5 anv\u00e4nda PKCE som ett extra f\u00f6rsvarslager, eftersom det skyddar mot scenarion d\u00e4r auktoriseringsserverns lagring av code_challenge inte angrips direkt men kodinterception sker i n\u00e4tverket.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"forutsattningar\">F\u00f6ruts\u00e4ttningar<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Innan du b\u00f6rjar beh\u00f6ver du dessa verktyg och kunskaper p\u00e5 plats:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Krav<\/th><th>Version \/ detalj<\/th><th>Syfte i projektet<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>22.x LTS (minst 18.x)<\/td><td>Runtime med inbyggd <code>crypto<\/code>-modul<\/td><\/tr><tr><td>npm<\/td><td>10.x<\/td><td>Pakethantering och skriptk\u00f6ring<\/td><\/tr><tr><td>Express<\/td><td>4.21.x<\/td><td>HTTP-server och routing<\/td><\/tr><tr><td>express-session<\/td><td>1.18.x<\/td><td>Sessionslagring server-side<\/td><\/tr><tr><td>axios<\/td><td>1.7.x<\/td><td>HTTP-klient f\u00f6r token-endpoint<\/td><\/tr><tr><td>dotenv<\/td><td>16.x<\/td><td>Milj\u00f6variabelhantering<\/td><\/tr><tr><td>OAuth 2.0-leverant\u00f6r<\/td><td>Google, GitHub, Okta eller Keycloak<\/td><td>Auktoriseringsserver<\/td><\/tr><tr><td>Grundl\u00e4ggande Express-kunskap<\/td><td>Routing, middleware<\/td><td>F\u00f6rst\u00e5else av kodbasen<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Du beh\u00f6ver ocks\u00e5 en registrerad applikation hos din valda OAuth-leverant\u00f6r. Vid registreringen s\u00e4tter du en godk\u00e4nd redirect URI, t.ex. <code>http:\/\/localhost:3000\/auth\/callback<\/code> f\u00f6r lokal utveckling. Fr\u00e5n leverant\u00f6ren f\u00e5r du ett <strong>Client ID<\/strong> och eventuellt ett <strong>Client Secret<\/strong>. Public clients som SPA:er och mobilappar registreras utan client_secret och f\u00f6rlitar sig helt p\u00e5 PKCE f\u00f6r s\u00e4kerheten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Kontrollera din Node.js-version innan du b\u00f6rjar:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# F\u00f6rv\u00e4ntad output: v22.x.x (eller v20.x.x som minimum)\n\nnpm --version\n# F\u00f6rv\u00e4ntad output: 10.x.x<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"steg-1-3-projektsetup-och-installation\">Steg 1-3: Projektsetup och installation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa projektkatalogen och installera beroendena:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir oauth2-pkce-demo && cd oauth2-pkce-demo\nnpm init -y\nnpm install express express-session axios dotenv\nnpm install --save-dev nodemon<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa filstrukturen som h\u00e5ller projektet v\u00e4lorganiserat:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>oauth2-pkce-demo\/\n\u251c\u2500\u2500 .env                   # Hemliga konfigurationsv\u00e4rden (l\u00e4ggs aldrig i git)\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 package.json\n\u2514\u2500\u2500 src\/\n    \u251c\u2500\u2500 server.js          # Express-server och grundkonfiguration\n    \u251c\u2500\u2500 pkce.js            # PKCE-nyckelgenerering\n    \u251c\u2500\u2500 routes\/\n    \u2502   \u251c\u2500\u2500 auth.js        # \/auth\/login, \/auth\/callback, \/auth\/logout\n    \u2502   \u2514\u2500\u2500 protected.js   # Skyddade API-rutter\n    \u2514\u2500\u2500 middleware\/\n        \u2514\u2500\u2500 requireAuth.js # Autentiserings- och token-validering<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa <code>.env<\/code>-filen med dina OAuth-uppgifter. Byt ut exempelv\u00e4rdena mot riktiga v\u00e4rden fr\u00e5n din leverant\u00f6r:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nSESSION_SECRET=byt-ut-detta-till-ett-langt-slumpmassigt-varde-minst-32-tecken\nCLIENT_ID=din-client-id-har\nCLIENT_SECRET=din-client-secret-har\n\n# Redirect URI m\u00e5ste matcha exakt vad du registrerat hos leverant\u00f6ren\nREDIRECT_URI=http:\/\/localhost:3000\/auth\/callback\n\n# Google OAuth-slutpunkter (byt ut mot din leverant\u00f6rs URL:er)\nAUTHORIZATION_ENDPOINT=https:\/\/accounts.google.com\/o\/oauth2\/v2\/auth\nTOKEN_ENDPOINT=https:\/\/oauth2.googleapis.com\/token\nUSERINFO_ENDPOINT=https:\/\/openidconnect.googleapis.com\/v1\/userinfo\n\n# Scopes: openid aktiverar OIDC-l\u00e4get med id_token\nSCOPES=openid email profile\n\nPORT=3000\nNODE_ENV=development<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00e4gg omedelbart till <code>.env<\/code> i <code>.gitignore<\/code>. Det \u00e4r det vanligaste misstaget som leder till att API-nycklar exponeras i git-historiken:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>printf \".env\\nnode_modules\/\\n\" > .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Uppdatera <code>package.json<\/code> med startskript och typ-f\u00e4lt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"oauth2-pkce-demo\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"start\": \"node src\/server.js\",\n    \"dev\": \"nodemon src\/server.js\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.7.0\",\n    \"dotenv\": \"^16.0.0\",\n    \"express\": \"^4.21.0\",\n    \"express-session\": \"^1.18.0\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^3.0.0\"\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"steg-4-5-serverkonfiguration-och-sessionshantering\">Steg 4-5: Serverkonfiguration och sessionshantering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa <code>src\/server.js<\/code>. Det \u00e4r startpunkten som kopplar ihop Express, sessioner och rutter. Varje sessionskonfigurationsparameter har en direkt s\u00e4kerhetsp\u00e5verkan:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/server.js\nrequire('dotenv').config();\nconst express = require('express');\nconst session = require('express-session');\nconst authRoutes = require('.\/routes\/auth');\nconst protectedRoutes = require('.\/routes\/protected');\n\nconst app = express();\nconst PORT = process.env.PORT || 3000;\n\n\/\/ Lita p\u00e5 proxy om du k\u00f6r bakom Nginx eller Cloudflare i produktion\napp.set('trust proxy', 1);\n\n\/\/ Sessionskonfiguration\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    secure: process.env.NODE_ENV === 'production', \/\/ Kr\u00e4v HTTPS i produktion\n    httpOnly: true,   \/\/ F\u00f6rhindrar \u00e5tkomst via JavaScript (skyddar mot XSS)\n    sameSite: 'lax',  \/\/ Till\u00e5ter redirect-cookies men blockerar cross-site POST\n    maxAge: 1000 * 60 * 60 * 24 \/\/ 24 timmar\n  }\n}));\n\n\/\/ Rutter\napp.use('\/auth', authRoutes);\napp.use('\/api', protectedRoutes);\n\n\/\/ Startsida\napp.get('\/', (req, res) => {\n  if (req.session.user) {\n    return res.send(`\n      <h1>Inloggad som ${escapeHtml(req.session.user.email)}<\/h1>\n      <a href=\"\/api\/profile\">Visa profil<\/a> |\n      <a href=\"\/auth\/logout\">Logga ut<\/a>\n    `);\n  }\n  res.send('<h1>OAuth 2.0 + PKCE Demo<\/h1><a href=\"\/auth\/login\">Logga in<\/a>');\n});\n\n\/\/ Enkel HTML-escape f\u00f6r att f\u00f6rhindra XSS i svar\nfunction escapeHtml(str) {\n  return String(str)\n    .replace(\/&\/g, '&amp;')\n    .replace(\/<\/g, '&lt;')\n    .replace(\/>\/g, '&gt;')\n    .replace(\/\"\/g, '&quot;');\n}\n\napp.listen(PORT, () => {\n  console.log(`Server k\u00f6rs p\u00e5 http:\/\/localhost:${PORT}`);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Sessionsinst\u00e4llningen <code>httpOnly: true<\/code> \u00e4r kritisk. Den hindrar JavaScript fr\u00e5n att l\u00e4sa sessionscookien, vilket eliminerar den vanligaste XSS-attackvektorn mot sessionstoken. <code>sameSite: 'lax'<\/code> till\u00e5ter att cookien skickas med navigationsredirects, vilket OAuth-fl\u00f6det kr\u00e4ver, men blockerar cookien vid cross-site POST-f\u00f6rfr\u00e5gningar. L\u00e4gg m\u00e4rke till att vi k\u00f6r med <code>secure: false<\/code> lokalt under utveckling men s\u00e4tter <code>secure: true<\/code> i produktion via milj\u00f6variabeln.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Observera \u00e4ven <code>escapeHtml<\/code>-funktionen i startsidan. Att skriva ut <code>req.session.user.email<\/code> direkt i HTML utan escape skapar en stored XSS-s\u00e5rbarhet om ett konto registrerats med ett epostnamn som inneh\u00e5ller HTML-tecken. Det \u00e4r ett litet till\u00e4gg med stor s\u00e4kerhetsp\u00e5verkan.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"steg-6-pkce-nyckelgenerering\">Steg 6: PKCE-nyckelgenerering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa <code>src\/pkce.js<\/code> med den kryptografiska logiken. Node.js inbyggda <code>crypto<\/code>-modul hanterar allt. Inga externa beroenden beh\u00f6vs f\u00f6r den h\u00e4r k\u00e4rndelen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/pkce.js\nconst crypto = require('crypto');\n\n\/\/ Genererar ett kryptografiskt s\u00e4kert code_verifier.\n\/\/ RFC 7636 kr\u00e4ver 43-128 tecken fr\u00e5n alfabetet [A-Z a-z 0-9 - . _ ~].\n\/\/ 32 slumpm\u00e4ssiga bytes base64url-kodade ger exakt 43 tecken utan padding.\nfunction generateCodeVerifier() {\n  return crypto.randomBytes(32).toString('base64url');\n}\n\n\/\/ Ber\u00e4knar code_challenge fr\u00e5n code_verifier med S256-metoden.\n\/\/ S256 = BASE64URL(SHA256(ASCII(code_verifier)))\n\/\/ Auktoriseringsservern lagrar code_challenge och verifierar\n\/\/ att SHA256(verifier) == challenge vid tokenutbytet.\nfunction generateCodeChallenge(codeVerifier) {\n  const hash = crypto.createHash('sha256').update(codeVerifier).digest();\n  return hash.toString('base64url');\n}\n\n\/\/ Genererar ett kryptografiskt slumpm\u00e4ssigt state-v\u00e4rde.\n\/\/ State-parametern skyddar mot CSRF-attacker i OAuth-fl\u00f6det.\nfunction generateState() {\n  return crypto.randomBytes(16).toString('hex');\n}\n\nmodule.exports = { generateCodeVerifier, generateCodeChallenge, generateState };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Varf\u00f6r <code>base64url<\/code> och inte standard base64? Standard base64 anv\u00e4nder tecknen <code>+<\/code>, <code>\/<\/code> och <code>=<\/code> som m\u00e5ste URL-encodas. Base64url ers\u00e4tter dem med <code>-<\/code>, <code>_<\/code> och utel\u00e4mnar padding, vilket ger en str\u00e4ng som \u00e4r direkt s\u00e4ker att inkludera i URL-parametrar utan ytterligare encoding. RFC 7636 specificerar explicit base64url-encoding f\u00f6r b\u00e5de code_verifier och code_challenge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Undvik <code>'plain'<\/code>-metoden f\u00f6r code_challenge. Den skickar code_verifier i klartext som code_challenge, vilket eliminerar PKCE:s skydd helt. En angripare som f\u00e5ngade auktoriseringsf\u00f6rfr\u00e5gan har d\u00e5 allt som beh\u00f6vs f\u00f6r att byta koden mot tokens. RFC 9700 rekommenderar att auktoriseringsservrar avvisar <code>plain<\/code>-metoden och bara accepterar <code>S256<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"steg-7-8-auktoriseringsflode-och-redirect\">Steg 7-8: Auktoriseringsfl\u00f6de och redirect<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa <code>src\/routes\/auth.js<\/code>. Den hanterar tre slutpunkter: <code>\/login<\/code> som startar PKCE-fl\u00f6det, <code>\/callback<\/code> som tar emot auktoriseringskoden fr\u00e5n leverant\u00f6ren, och <code>\/logout<\/code> som avslutar sessionen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/routes\/auth.js\nconst express = require('express');\nconst axios = require('axios');\nconst { generateCodeVerifier, generateCodeChallenge, generateState } = require('..\/pkce');\n\nconst router = express.Router();\n\n\/\/ Steg 7: Starta OAuth 2.0 + PKCE-fl\u00f6det\nrouter.get('\/login', (req, res) => {\n  const codeVerifier = generateCodeVerifier();\n  const codeChallenge = generateCodeChallenge(codeVerifier);\n  const state = generateState();\n\n  \/\/ Spara code_verifier och state i sessionen INNAN redirect.\n  \/\/ code_verifier skickas ALDRIG till auktoriseringsservern.\n  req.session.codeVerifier = codeVerifier;\n  req.session.oauthState = state;\n\n  const params = new URLSearchParams({\n    client_id: process.env.CLIENT_ID,\n    redirect_uri: process.env.REDIRECT_URI,\n    response_type: 'code',\n    scope: process.env.SCOPES,\n    state: state,\n    code_challenge: codeChallenge,\n    code_challenge_method: 'S256'\n  });\n\n  const authUrl = `${process.env.AUTHORIZATION_ENDPOINT}?${params}`;\n  res.redirect(authUrl);\n});\n\n\/\/ Steg 8: Hantera callback fr\u00e5n auktoriseringsservern\nrouter.get('\/callback', async (req, res) => {\n  const { code, state, error, error_description } = req.query;\n\n  \/\/ Kontrollera om auktoriseringsservern returnerade ett fel\n  if (error) {\n    console.error('OAuth-fel:', error, error_description);\n    return res.status(400).send(`Auktoriseringsfel: ${error}`);\n  }\n\n  \/\/ Verifiera state-parametern f\u00f6r att f\u00f6rhindra CSRF-attacker\n  if (!state || state !== req.session.oauthState) {\n    return res.status(403).send('Ogiltig state-parameter. M\u00f6jlig CSRF-attack.');\n  }\n\n  \/\/ H\u00e4mta code_verifier fr\u00e5n sessionen\n  const codeVerifier = req.session.codeVerifier;\n  if (!codeVerifier) {\n    return res.status(400).send('Saknad code_verifier. B\u00f6rja om inloggningsprocessen.');\n  }\n\n  try {\n    \/\/ Byt auktoriseringskoden mot access_token och refresh_token\n    const tokenResponse = await axios.post(\n      process.env.TOKEN_ENDPOINT,\n      new URLSearchParams({\n        grant_type: 'authorization_code',\n        client_id: process.env.CLIENT_ID,\n        client_secret: process.env.CLIENT_SECRET, \/\/ Utel\u00e4mna f\u00f6r public clients\n        code: code,\n        redirect_uri: process.env.REDIRECT_URI,\n        code_verifier: codeVerifier \/\/ PKCE: verifier som auktoriseringsservern kontrollerar\n      }),\n      { headers: { 'Content-Type': 'application\/x-www-form-urlencoded' } }\n    );\n\n    const { access_token, refresh_token, expires_in } = tokenResponse.data;\n\n    \/\/ Rensa PKCE-data fr\u00e5n sessionen direkt efter lyckad utbyte\n    delete req.session.codeVerifier;\n    delete req.session.oauthState;\n\n    \/\/ H\u00e4mta anv\u00e4ndarinformation med access_token\n    const userResponse = await axios.get(process.env.USERINFO_ENDPOINT, {\n      headers: { Authorization: `Bearer ${access_token}` }\n    });\n\n    \/\/ Spara autentiseringsdata i sessionen\n    req.session.user = userResponse.data;\n    req.session.accessToken = access_token;\n    req.session.refreshToken = refresh_token || null;\n    req.session.tokenExpiry = Date.now() + (expires_in * 1000);\n\n    res.redirect('\/');\n  } catch (err) {\n    const errData = err.response?.data;\n    console.error('Token-utbytesfel:', errData || err.message);\n    res.status(500).send('Autentisering misslyckades. F\u00f6rs\u00f6k igen.');\n  }\n});\n\n\/\/ Steg 12: Logga ut och f\u00f6rst\u00f6r sessionen\nrouter.get('\/logout', (req, res) => {\n  req.session.destroy((err) => {\n    if (err) {\n      return res.status(500).send('Utloggning misslyckades.');\n    }\n    res.clearCookie('connect.sid');\n    res.redirect('\/');\n  });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">State-valideringen p\u00e5 raderna 35-38 \u00e4r obligatorisk. Utan den kan en angripare lura din app att acceptera en auktoriseringskod fr\u00e5n en annan anv\u00e4ndares inloggningssession. Det klassiska OAuth CSRF-angreppet fungerar s\u00e5: angriparen initierar ett inloggningsfl\u00f6de, avbryter det och ger offret en l\u00e4nk till callback-URL:en med angriparens kod. Offrets webbl\u00e4sare skickar offrets session-cookie till din callback, och om du inte kontrollerar state loggas offret in som angriparen. Varje nytt inloggningsf\u00f6rs\u00f6k genererar ett nytt, unikt state-v\u00e4rde som bara g\u00e4ller f\u00f6r den specifika sessionen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"steg-9-autentiseringsmiddleware-och-skyddade-rutter\">Steg 9: Autentiseringsmiddleware och skyddade rutter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa middleware som kontrollerar b\u00e5de autentisering och token-giltighet, med automatisk f\u00f6rnyelse via refresh_token:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware\/requireAuth.js\nconst axios = require('axios');\n\nasync function requireAuth(req, res, next) {\n  \/\/ Kontrollera att anv\u00e4ndaren \u00e4r inloggad\n  if (!req.session.user || !req.session.accessToken) {\n    return res.status(401).json({\n      error: 'Inte autentiserad.',\n      loginUrl: '\/auth\/login'\n    });\n  }\n\n  \/\/ F\u00f6rnya token 60 sekunder innan den g\u00e5r ut f\u00f6r att undvika race conditions\n  const bufferMs = 60 * 1000;\n  const tokenExpiresSoon = req.session.tokenExpiry &&\n    Date.now() > req.session.tokenExpiry - bufferMs;\n\n  if (tokenExpiresSoon) {\n    if (!req.session.refreshToken) {\n      req.session.destroy(() => {});\n      return res.status(401).json({\n        error: 'Session utg\u00e5ngen.',\n        loginUrl: '\/auth\/login'\n      });\n    }\n\n    try {\n      const refreshResponse = await axios.post(\n        process.env.TOKEN_ENDPOINT,\n        new URLSearchParams({\n          grant_type: 'refresh_token',\n          client_id: process.env.CLIENT_ID,\n          client_secret: process.env.CLIENT_SECRET,\n          refresh_token: req.session.refreshToken\n        }),\n        { headers: { 'Content-Type': 'application\/x-www-form-urlencoded' } }\n      );\n\n      const { access_token, refresh_token, expires_in } = refreshResponse.data;\n      req.session.accessToken = access_token;\n      \/\/ Vissa leverant\u00f6rer roterar refresh_token; beh\u00e5ll gamla om ny saknas\n      req.session.refreshToken = refresh_token || req.session.refreshToken;\n      req.session.tokenExpiry = Date.now() + (expires_in * 1000);\n    } catch (err) {\n      req.session.destroy(() => {});\n      return res.status(401).json({\n        error: 'Token-f\u00f6rnyelse misslyckades.',\n        loginUrl: '\/auth\/login'\n      });\n    }\n  }\n\n  next();\n}\n\nmodule.exports = requireAuth;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Skapa de skyddade rutterna i <code>src\/routes\/protected.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/routes\/protected.js\nconst express = require('express');\nconst requireAuth = require('..\/middleware\/requireAuth');\n\nconst router = express.Router();\n\n\/\/ Till\u00e4mpa autentiseringsmiddleware p\u00e5 alla \/api-rutter\nrouter.use(requireAuth);\n\nrouter.get('\/profile', (req, res) => {\n  res.json({\n    message: 'Profildata h\u00e4mtad',\n    user: {\n      sub: req.session.user.sub,\n      name: req.session.user.name,\n      email: req.session.user.email\n    },\n    tokenExpiry: new Date(req.session.tokenExpiry).toISOString()\n  });\n});\n\nrouter.get('\/dashboard', (req, res) => {\n  res.json({\n    message: `V\u00e4lkommen tillbaka, ${req.session.user.name || req.session.user.email}`,\n    scopes: process.env.SCOPES.split(' ')\n  });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Observera att vi returnerar ett minimalt subset av anv\u00e4ndardata fr\u00e5n <code>\/api\/profile<\/code>. Vi undviker att exponera hela <code>req.session.user<\/code>-objektet direkt eftersom det kan inneh\u00e5lla mer information fr\u00e5n leverant\u00f6ren \u00e4n vad frontend-koden beh\u00f6ver. Det minimerar datam\u00e4ngden som exponeras vid eventuell IDOR-s\u00e5rbarhet. L\u00e4s mer om dessa m\u00f6nster i v\u00e5r guide om <a href=\"\/se\/owasp-top-10-nodejs\/\">OWASP Top 10 i Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"steg-10-11-testning-av-hela-oauth-flodet\">Steg 10-11: Testning av hela OAuth-fl\u00f6det<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Starta servern i utvecklingsl\u00e4ge:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run dev\n\n# F\u00f6rv\u00e4ntad terminal-output:\n# [nodemon] starting `node src\/server.js`\n# Server k\u00f6rs p\u00e5 http:\/\/localhost:3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00d6ppna webbl\u00e4saren och navigera till <code>http:\/\/localhost:3000<\/code>. Klicka p\u00e5 &#8220;Logga in&#8221;. Du omdirigeras till auktoriseringsservern. Granska URL-parametrarna i adressf\u00e4ltet. Du ska se <code>code_challenge<\/code>, <code>code_challenge_method=S256<\/code> och <code>state<\/code>. Du ser <strong>aldrig<\/strong> <code>code_verifier<\/code> i URL:en, den existerar bara i din server-session.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Efter godk\u00e4nd inloggning omdirigeras du till <code>\/auth\/callback?code=...&state=...<\/code>. Servern verifierar state, h\u00e4mtar code_verifier fr\u00e5n sessionen och byter auktoriseringskoden mot tokens. Du hamnar tillbaka p\u00e5 startsidan inloggad.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Testa det skyddade API:et med curl. Du beh\u00f6ver en aktiv session-cookie fr\u00e5n webbl\u00e4sarens developer tools, eller spara den under inloggning:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Spara session-cookie under inloggning\ncurl -c \/tmp\/oauth-cookies.txt -L http:\/\/localhost:3000\/auth\/login\n\n# Testa skyddad rutt med sparad cookie\ncurl -b \/tmp\/oauth-cookies.txt http:\/\/localhost:3000\/api\/profile\n\n# F\u00f6rv\u00e4ntad JSON-respons:\n{\n  \"message\": \"Profildata h\u00e4mtad\",\n  \"user\": {\n    \"sub\": \"110169484474386276334\",\n    \"name\": \"Anna Lindqvist\",\n    \"email\": \"anna@example.se\"\n  },\n  \"tokenExpiry\": \"2026-06-18T16:30:00.000Z\"\n}\n\n# Test utan cookie: ska returnera 401\ncurl http:\/\/localhost:3000\/api\/profile\n# {\"error\":\"Inte autentiserad.\",\"loginUrl\":\"\/auth\/login\"}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Observera att curl-fl\u00f6det kr\u00e4ver att du manuellt godk\u00e4nner inloggning i webbl\u00e4saren eftersom auktoriseringsservern presenterar sitt eget formul\u00e4r. I automatiserade integrationstester anv\u00e4nds vanligtvis en test-OAuth-server som <strong>oidc-provider<\/strong> eller en mock-implementation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"komplett-projektoversikt-hela-dataflodet\">Komplett projekt\u00f6versikt: hela datafl\u00f6det<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">H\u00e4r \u00e4r en fullst\u00e4ndig bild av hur data fl\u00f6dar genom systemet fr\u00e5n f\u00f6rsta click till aktiv session:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Steg<\/th><th>Akt\u00f6r<\/th><th>\u00c5tg\u00e4rd<\/th><th>Data som skickas<\/th><th>Data som sparas<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>Browser<\/td><td>GET \/auth\/login<\/td><td>Session-cookie<\/td><td>Ingenting<\/td><\/tr><tr><td>2<\/td><td>Node.js-server<\/td><td>Genererar verifier, challenge och state<\/td><td>Ingenting externt<\/td><td>code_verifier, oauthState i session<\/td><\/tr><tr><td>3<\/td><td>Node.js-server<\/td><td>Redirect till auktoriseringsserver<\/td><td>client_id, code_challenge, S256, state, scope<\/td><td>Ingenting nytt<\/td><\/tr><tr><td>4<\/td><td>Anv\u00e4ndare<\/td><td>Loggar in och godk\u00e4nner<\/td><td>Inloggningsuppgifter till auktoriseringsservern<\/td><td>Hos auktoriseringsservern<\/td><\/tr><tr><td>5<\/td><td>Auktoriseringsserver<\/td><td>Redirect till \/auth\/callback<\/td><td>code (eng\u00e5ngsbruk), state<\/td><td>Ingenting p\u00e5 klienten<\/td><\/tr><tr><td>6<\/td><td>Node.js-server<\/td><td>Verifierar state, h\u00e4mtar code_verifier ur session<\/td><td>Intern sessionslookup<\/td><td>Ingenting nytt<\/td><\/tr><tr><td>7<\/td><td>Node.js-server<\/td><td>POST till token_endpoint<\/td><td>code, code_verifier, client_id, redirect_uri<\/td><td>Ingenting \u00e4nnu<\/td><\/tr><tr><td>8<\/td><td>Auktoriseringsserver<\/td><td>Verifierar SHA256(verifier) == challenge<\/td><td>Returnerar access_token, refresh_token<\/td><td>Hos auktoriseringsservern<\/td><\/tr><tr><td>9<\/td><td>Node.js-server<\/td><td>GET userinfo med access_token<\/td><td>Bearer-token i header<\/td><td>user, accessToken, refreshToken, tokenExpiry i session<\/td><\/tr><tr><td>10<\/td><td>Node.js-server<\/td><td>Rensar PKCE-data, redirect till \/<\/td><td>Session-cookie till browser<\/td><td>code_verifier och oauthState raderas<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vanliga-misstag-som-kostar-tid\">Vanliga misstag som kostar tid<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dessa sex misstag uppst\u00e5r konsekvent i OAuth 2.0 + PKCE-implementationer och leder till antingen s\u00e4kerhetsh\u00e5l eller brutna fl\u00f6den.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"misstag-1-skickar-code_verifier-i-auktoriseringsforfragan\">Misstag 1: Skickar code_verifier i auktoriseringsf\u00f6rfr\u00e5gan<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Det absolut vanligaste misstaget bland utvecklare som l\u00e4ser PKCE f\u00f6r f\u00f6rsta g\u00e5ngen: skicka <code>code_verifier<\/code> ist\u00e4llet f\u00f6r <code>code_challenge<\/code> i steg 3. Auktoriseringsservern tar emot en parameter den inte f\u00f6rst\u00e5r, fl\u00f6det misslyckas med <code>invalid_request<\/code>, och felmeddelandena ger s\u00e4llan tydlig v\u00e4gledning. Regeln \u00e4r enkel: <code>code_challenge<\/code> g\u00e5r till auktoriseringsservern i steg 3. <code>code_verifier<\/code> g\u00e5r till token-endpoint i steg 7. Aldrig tv\u00e4rtom.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"misstag-2-ateranvander-code_verifier-mellan-sessioner\">Misstag 2: \u00c5teranv\u00e4nder code_verifier mellan sessioner<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RFC 7636 kr\u00e4ver att code_verifier \u00e4r <strong>ny f\u00f6r varje auktoriseringsf\u00f6rfr\u00e5gan<\/strong>. Om du lagrar code_verifier i en global variabel ist\u00e4llet f\u00f6r i sessionen kan ett race condition uppst\u00e5: tv\u00e5 parallella inloggningar blandar ihop varandras verifiers. Det leder till sporadiska <code>invalid_grant<\/code>-fel som \u00e4r n\u00e4stintill om\u00f6jliga att reproducera i lokal utveckling men dyker upp regelbundet under h\u00f6g trafik i produktion.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"misstag-3-hoppar-over-state-valideringen\">Misstag 3: Hoppar \u00f6ver state-valideringen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">State-parametern \u00e4r inte valfri. Utan state-validering i callback-hanteraren exponerar du appen f\u00f6r OAuth CSRF-attacker. <a href=\"https:\/\/owasp.org\/www-community\/attacks\/csrf\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP dokumenterar detta angreppsm\u00f6nster<\/a> som ett av de vanligaste OAuth-s\u00e4kerhetsproblemen. Resultatet av en lyckad attack \u00e4r att angriparen loggas in som ett offer i din app, eller att offrets konto kopplas till angriparens externa identitet.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"misstag-4-lagrar-access_token-i-localstorage\">Misstag 4: Lagrar access_token i localStorage<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">M\u00e5nga tutorials visar att man sparar access_token i webbl\u00e4sarens <code>localStorage<\/code> f\u00f6r att sedan skicka den med API-anrop via JavaScript. Det \u00e4r ett allvarligt s\u00e4kerhetsfel. localStorage \u00e4r \u00e5tkomlig fr\u00e5n all JavaScript p\u00e5 sidan, vilket inneb\u00e4r att en enda XSS-s\u00e5rbarhet ger angriparen tillg\u00e5ng till token. Lagra alltid token server-side i en HTTP-only session-cookie, som vi g\u00f6r i den h\u00e4r implementationen. Se v\u00e5r guide om <a href=\"\/se\/nodejs-session-management\/\">Node.js sessionshantering<\/a> f\u00f6r detaljer om s\u00e4ker cookiekonfiguration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"misstag-5-anvander-plain-som-code_challenge_method\">Misstag 5: Anv\u00e4nder &#8216;plain&#8217; som code_challenge_method<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Plain-metoden skickar code_verifier som code_challenge i klartext. Det inneb\u00e4r att auktoriseringsservern tar emot den faktiska verifieraren redan i steg 3, och en angripare som f\u00e5ngar upp auktoriseringsf\u00f6rfr\u00e5gan har allt som beh\u00f6vs f\u00f6r att byta koden mot tokens. Plain-metoden existerar bara f\u00f6r bak\u00e5tkompatibilitet med \u00e4ldre system. Moderna auktoriseringsservrar ska konfigureras att avvisa plain och bara acceptera S256.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"misstag-6-glommer-att-rensa-pkce-data-efter-tokenutbytet\">Misstag 6: Gl\u00f6mmer att rensa PKCE-data efter tokenutbytet<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">code_verifier och oauthState ska raderas ur sessionen direkt efter att tokenutbytet lyckats. Om du beh\u00e5ller dem uppst\u00e5r f\u00f6rvirring om anv\u00e4ndaren startar ett nytt inloggningsfl\u00f6de utan att logga ut, och du riskerar att sessionen inneh\u00e5ller stale PKCE-data fr\u00e5n tidigare fl\u00f6den. I v\u00e5r implementation hanteras detta med <code>delete req.session.codeVerifier<\/code> och <code>delete req.session.oauthState<\/code> direkt efter lyckad token-h\u00e4mtning.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"felsokningsguide-8-vanliga-oauth-fel\">Fels\u00f6kningsguide: 8 vanliga OAuth-fel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dessa \u00e5tta fel dyker upp regelbundet i PKCE-implementationer. H\u00e4r \u00e4r symptomen, orsakerna och l\u00f6sningarna:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Felmeddelande<\/th><th>HTTP-kod<\/th><th>Vanligaste orsak<\/th><th>L\u00f6sning<\/th><\/tr><\/thead><tbody><tr><td><code>invalid_grant<\/code><\/td><td>400<\/td><td>code_verifier matchar inte challenge, eller koden har redan anv\u00e4nts<\/td><td>Verifiera att SHA256(verifier) ger r\u00e4tt challenge. Auktoriseringskoder \u00e4r eng\u00e5ngsbruk.<\/td><\/tr><tr><td><code>invalid_request<\/code><\/td><td>400<\/td><td>Redirect URI ej registrerad, eller saknad PKCE-parameter<\/td><td>Kontrollera att redirect_uri matchar exakt vad du registrerat hos leverant\u00f6ren, tecken f\u00f6r tecken.<\/td><\/tr><tr><td><code>invalid_client<\/code><\/td><td>401<\/td><td>Fel client_id eller client_secret<\/td><td>Kontrollera .env-v\u00e4rdena. Koda aldrig in dem direkt i k\u00e4llkoden.<\/td><\/tr><tr><td>State mismatch (din 403)<\/td><td>403<\/td><td>Sessionen gick ut mellan login och callback, eller cookies blockeras<\/td><td>Kontrollera att sameSite=&#8217;lax&#8217; \u00e4r satt och att sessionen lever l\u00e4nge nog f\u00f6r inloggningsfl\u00f6det.<\/td><\/tr><tr><td>CORS-fel vid token-endpoint<\/td><td>Webbl\u00e4sarfel<\/td><td>Token-request g\u00f6rs direkt fr\u00e5n frontend-JavaScript<\/td><td>Token-request M\u00c5STE g\u00f6ras server-side. Flytta axios.post-anropet till Node.js-backend.<\/td><\/tr><tr><td><code>access_denied<\/code><\/td><td>400<\/td><td>Anv\u00e4ndaren nekade \u00e5tkomst, eller scope saknas i leverant\u00f6rens konfiguration<\/td><td>Kontrollera att beg\u00e4rda scopes \u00e4r aktiverade i leverant\u00f6rens app-konfiguration.<\/td><\/tr><tr><td>Cannot read properties of undefined (codeVerifier)<\/td><td>500<\/td><td>Session initialiserades inte, eller express-session saknas<\/td><td>Verifiera att app.use(session(&#8230;)) k\u00f6rs INNAN routing-middleware i server.js.<\/td><\/tr><tr><td><code>redirect_uri_mismatch<\/code><\/td><td>400<\/td><td>URI i koden matchar inte registrerad URI hos leverant\u00f6ren<\/td><td>URI:n m\u00e5ste matcha tecken f\u00f6r tecken inklusive avslutande snedstreck och PORT-nummer.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00f6r djupare fels\u00f6kning, aktivera tempor\u00e4r logging av OAuth-parametrarna. Logga aldrig code_verifier, access_token eller refresh_token i produktionsloggar:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Fels\u00f6kningslogging: aktivera bara i development\nif (process.env.NODE_ENV === 'development') {\n  console.log('OAuth-parametrar:', {\n    authorization_endpoint: process.env.AUTHORIZATION_ENDPOINT,\n    client_id: process.env.CLIENT_ID,\n    redirect_uri: process.env.REDIRECT_URI,\n    scope: process.env.SCOPES,\n    code_challenge_method: 'S256',\n    \/\/ Logga ALDRIG code_verifier, access_token eller refresh_token\n    code_challenge_preview: codeChallenge.substring(0, 10) + '...'\n  });\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerade-tekniker-och-produktionsinstallningar\">Avancerade tekniker och produktionsinst\u00e4llningar<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Grundimplementationen fungerar f\u00f6r prototyping och inl\u00e4rning. Produktionsmilj\u00f6er kr\u00e4ver ytterligare h\u00e4rdning p\u00e5 fyra niv\u00e5er:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"persistent-sessionslagring-med-redis\">Persistent sessionslagring med Redis<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Minnesbaserad sessionslagring f\u00f6rsvinner vid serveromstart. F\u00f6r produktionsmilj\u00f6er med flera instanser kr\u00e4vs en delad sessionslagring som Redis. Installera <strong>connect-redis<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install connect-redis redis\n\n\/\/ src\/server.js - produktionskonfiguration\nconst RedisStore = require('connect-redis').default;\nconst { createClient } = require('redis');\n\nconst redisClient = createClient({ url: process.env.REDIS_URL });\nredisClient.connect().catch(console.error);\n\napp.use(session({\n  store: new RedisStore({ client: redisClient }),\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    secure: true,          \/\/ Kr\u00e4v HTTPS i produktion\n    httpOnly: true,\n    sameSite: 'lax',\n    maxAge: 1000 * 60 * 60 * 8 \/\/ 8 timmar\n  }\n}));<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"openid-connect-id-token-validering\">OpenID Connect ID-token-validering<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Om auktoriseringsservern returnerar ett <code>id_token<\/code> (ett JWT med anv\u00e4ndarinformation via OpenID Connect), validera det innan du litar p\u00e5 inneh\u00e5llet. Biblioteket <strong>jose<\/strong> hanterar JWKS-baserad JWT-validering:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install jose\n\nconst { createRemoteJWKSet, jwtVerify } = require('jose');\n\nconst JWKS = createRemoteJWKSet(\n  new URL('https:\/\/accounts.google.com\/.well-known\/openid-configuration')\n);\n\nasync function validateIdToken(id_token) {\n  const { payload } = await jwtVerify(id_token, JWKS, {\n    issuer: 'https:\/\/accounts.google.com',\n    audience: process.env.CLIENT_ID\n  });\n  \/\/ Kontrollera nonce om du anv\u00e4nde en i auktoriseringsf\u00f6rfr\u00e5gan\n  return payload;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00e4s mer om JWT-validering i v\u00e5r guide om <a href=\"\/se\/jwt-authentication-nodejs\/\">JWT-autentisering i Node.js<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pkce-flodet-utan-client_secret-for-public-clients\">PKCE-fl\u00f6det utan client_secret f\u00f6r public clients<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Single-page applikationer och mobilappar kan inte s\u00e4kra ett client_secret; k\u00e4llkoden \u00e4r alltid tillg\u00e4nglig f\u00f6r anv\u00e4ndaren. F\u00f6r dessa public clients utel\u00e4mnas client_secret helt fr\u00e5n token-f\u00f6rfr\u00e5gan. PKCE ers\u00e4tter behovet av client_secret eftersom code_verifier bevisar att r\u00e4tt klient, den som skapade code_challenge, slutf\u00f6rde fl\u00f6det. Registrera appen som &#8220;public client&#8221; hos leverant\u00f6ren f\u00f6r att till\u00e5ta tokenutbyte utan secret.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hastighetsbegransning-pa-auth-endpoints\">Hastighetsbegr\u00e4nsning p\u00e5 auth-endpoints<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth-endpoints \u00e4r frekventa m\u00e5l f\u00f6r scanning och missbruk. L\u00e4gg till rate limiting p\u00e5 <code>\/auth\/login<\/code> och <code>\/auth\/callback<\/code>. V\u00e5r guide om <a href=\"\/se\/rate-limiting-nodejs\/\">rate limiting i Node.js<\/a> visar hur du konfigurerar express-rate-limit f\u00f6r just auth-fl\u00f6den. Kombinera med <a href=\"\/se\/two-factor-authentication-nodejs\/\">tv\u00e5faktorsautentisering i Node.js<\/a> f\u00f6r ett komplett autentiseringslager. Se \u00e4ven <a href=\"https:\/\/expressjs.com\/en\/advanced\/best-practice-security.html\" target=\"_blank\" rel=\"noopener noreferrer\">Express.js officiella s\u00e4kerhetsguide<\/a> f\u00f6r ytterligare produktionsh\u00e4rdning med Helmet.js och Content Security Policy. <a href=\"\/se\/csrf-protection-nodejs\/\">CSRF-skydd i Node.js<\/a> t\u00e4cker de attacker som OAuth-fl\u00f6dets state-parameter f\u00f6rhindrar i autentiseringsfl\u00f6det, men som kan dyka upp i andra delar av din app. Auth0:s dokumentation om <a href=\"https:\/\/auth0.com\/docs\/get-started\/authentication-and-authorization-flow\/authorization-code-flow-with-pkce\" target=\"_blank\" rel=\"noopener noreferrer\">auktoriseringskodfl\u00f6det med PKCE<\/a> ger en leverant\u00f6rsneutral referensimplementation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"relaterad-lasning\">Relaterad l\u00e4sning<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"relaterade-artiklar\">Relaterade artiklar<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n  <li><a href=\"\/se\/jwt-authentication-nodejs\/\">JWT-autentisering i Node.js: 10 steg<\/a> &#8211; tokenbaserad autentisering som kompletterar OAuth 2.0<\/li>\n  <li><a href=\"\/se\/csrf-protection-nodejs\/\">CSRF-skydd i Node.js: 12 steg<\/a> &#8211; de attacker som OAuth state-parametern f\u00f6rhindrar i auth-fl\u00f6det<\/li>\n  <li><a href=\"\/se\/nodejs-session-management\/\">Node.js sessionshantering: 11 steg<\/a> &#8211; djupg\u00e5ende sessionsarkitektur och s\u00e4ker cookiekonfiguration<\/li>\n  <li><a href=\"\/se\/two-factor-authentication-nodejs\/\">Tv\u00e5faktorsautentisering i Node.js: 11 steg<\/a> &#8211; l\u00e4gg till TOTP\/2FA ovanp\u00e5 OAuth-inloggning<\/li>\n  <li><a href=\"\/se\/rate-limiting-nodejs\/\">Rate Limiting i Node.js: 12 steg<\/a> &#8211; skydda auth-endpoints mot brute force och scanning<\/li>\n  <li><a href=\"\/se\/owasp-top-10-nodejs\/\">OWASP Top 10 i Node.js: 12 steg<\/a> &#8211; bredare s\u00e4kerhetskontext inklusive broken access control och injection<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vanliga-fragor-om-oauth-2-0-och-pkce\">Vanliga fr\u00e5gor om OAuth 2.0 och PKCE<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"behover-jag-pkce-om-jag-redan-har-en-client_secret\">Beh\u00f6ver jag PKCE om jag redan har en client_secret?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. RFC 9700 och OAuth 2.0 Security Best Current Practice rekommenderar PKCE f\u00f6r alla klienttyper inklusive konfidentiella klienter med client_secret. PKCE och client_secret \u00e4r kompletterande, inte alternativa, s\u00e4kerhetsmekanismer. Client_secret autentiserar klienten mot servern. PKCE binder auktoriseringskoden till den specifika session som initierade fl\u00f6det. B\u00e5da mekanismerna bidrar med distinkt skydd mot olika angreppsscenarier.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"vad-hander-om-auktoriseringsservern-inte-stoder-pkce\">Vad h\u00e4nder om auktoriseringsservern inte st\u00f6der PKCE?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Auktoriseringsservern returnerar <code>invalid_request<\/code> om den inte k\u00e4nner igen PKCE-parametrarna. I det l\u00e4get kan du anv\u00e4nda standard auktoriseringskodfl\u00f6det med client_secret, men b\u00f6r planera migration till en PKCE-kompatibel leverant\u00f6r. Alla moderna OAuth-implementationer, Google, GitHub, Okta, Keycloak och Auth0, st\u00f6der PKCE sedan flera \u00e5r tillbaka.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hur-lang-ska-code_verifier-vara\">Hur l\u00e5ng ska code_verifier vara?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RFC 7636 kr\u00e4ver 43-128 tecken. Kortare verifiers ger svagare entropi. L\u00e4ngre ger on\u00f6digt overhead. En verifier genererad fr\u00e5n 32 slumpm\u00e4ssiga bytes base64url-kodad ger exakt 43 tecken med tillr\u00e4cklig entropi f\u00f6r alla praktiska s\u00e4kerhetsbehov. Det \u00e4r vad implementationen ovan genererar med <code>crypto.randomBytes(32).toString('base64url')<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kan-jag-anvanda-pkce-med-github-oauth\">Kan jag anv\u00e4nda PKCE med GitHub OAuth?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub st\u00f6der PKCE f\u00f6r OAuth Apps och GitHub Apps sedan 2023. L\u00e4gg till <code>code_challenge<\/code> och <code>code_challenge_method=S256<\/code> i auktoriseringsf\u00f6rfr\u00e5gan till <code>https:\/\/github.com\/login\/oauth\/authorize<\/code> och skicka <code>code_verifier<\/code> vid tokenutbytet till <code>https:\/\/github.com\/login\/oauth\/access_token<\/code>. GitHub-dokumentationen markerar PKCE som rekommenderat f\u00f6r alla nya app-integrationer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"vad-ar-skillnaden-mellan-oauth-2-0-och-openid-connect\">Vad \u00e4r skillnaden mellan OAuth 2.0 och OpenID Connect?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 \u00e4r ett auktoriseringsramverk: det handlar om att ge din app tillst\u00e5nd att g\u00f6ra saker \u00e5 en anv\u00e4ndares v\u00e4gnar. OpenID Connect (OIDC) \u00e4r ett identitetslager ovanp\u00e5 OAuth 2.0 som l\u00e4gger till autentisering. OIDC returnerar ett <code>id_token<\/code>, ett JWT med verifierbar anv\u00e4ndarinformation, ut\u00f6ver access_token. Scope-v\u00e4rdet <code>openid<\/code> aktiverar OIDC-l\u00e4get. I implementationen ovan aktiverar du OIDC automatiskt genom att inkludera <code>openid<\/code> i SCOPES-milj\u00f6variabeln.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hur-hanterar-jag-logout-korrekt-i-oauth\">Hur hanterar jag logout korrekt i OAuth?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Komplett OAuth-logout kr\u00e4ver tre steg. Steg 1: f\u00f6rst\u00f6r den lokala Express-sessionen med <code>req.session.destroy()<\/code> och rensa session-cookien. Steg 2: omdirigera anv\u00e4ndaren till auktoriseringsserverns logout-endpoint f\u00f6r att ogiltigf\u00f6rklara access_token och refresh_token hos leverant\u00f6ren. Steg 3: verifiera att token faktiskt \u00e4r ogiltigf\u00f6rklarade genom ett test-anrop mot userinfo-endpoint. Utan steg 2 f\u00f6rblir token giltiga hos auktoriseringsservern trots att din lokala session \u00e4r raderad.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"vad-hander-om-code_verifier-lacker-ut\">Vad h\u00e4nder om code_verifier l\u00e4cker ut?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Om en angripare fick tillg\u00e5ng till code_verifier ur din session kunde de, i kombination med en interceptad auktoriseringskod, byta koden mot tokens. Det \u00e4r extremt sv\u00e5rt i praktiken eftersom det kr\u00e4ver samtidig \u00e5tkomst till sessionslagringen och n\u00e4tverkstrafiken. F\u00f6rsvarslinjerna: anv\u00e4nd Redis-baserade sessioner (inte minnessessioner) med begr\u00e4nsad TTL, s\u00e4kerst\u00e4ll httpOnly p\u00e5 sessionscookien, och radera code_verifier ur sessionen omedelbart efter lyckad token-utbyte, vilket implementationen ovan g\u00f6r.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"registrera-din-app-hos-google-oauth-2-0\">Registrera din app hos Google OAuth 2.0<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Google \u00e4r den vanligaste OAuth-leverant\u00f6ren f\u00f6r 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\u00f6ver. H\u00e4r \u00e4r den exakta proceduren f\u00f6r att komma ig\u00e5ng med Google Identity Platform.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Navigera till <strong>Google Cloud Console<\/strong> och skapa ett nytt projekt om du inte redan har ett. Under menyn &#8220;APIs &amp; Services&#8221; v\u00e4ljer du &#8220;OAuth consent screen&#8221;. V\u00e4lj &#8220;External&#8221; som anv\u00e4ndartyp om du vill att externa Google-konton ska kunna logga in. Fyll i applikationsnamnet, din e-postadress och eventuellt en logotyp. Under &#8220;Scopes&#8221; l\u00e4gger du till de tre standardscopes som implementationen beh\u00f6ver: <code>openid<\/code>, <code>email<\/code> och <code>profile<\/code>. Dessa ger tillg\u00e5ng till anv\u00e4ndarens namn, e-postadress och profilbild utan att beg\u00e4ra tillg\u00e5ng till Drive, Gmail eller andra k\u00e4nsliga resurser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">G\u00e5 sedan till &#8220;Credentials&#8221; och klicka &#8220;Create Credentials&#8221; f\u00f6ljt av &#8220;OAuth 2.0 Client IDs&#8221;. V\u00e4lj applikationstypen &#8220;Web application&#8221;. Under &#8220;Authorized redirect URIs&#8221; l\u00e4gger du till din callback-URL. Beroende p\u00e5 milj\u00f6 kan du beh\u00f6va l\u00e4gga till flera URI:er:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Milj\u00f6<\/th><th>Redirect URI att registrera<\/th><th>Kommentar<\/th><\/tr><\/thead><tbody><tr><td>Lokal utveckling<\/td><td>http:\/\/localhost:3000\/auth\/callback<\/td><td>HTTP till\u00e5tet f\u00f6r localhost<\/td><\/tr><tr><td>Staging<\/td><td>https:\/\/staging.din-app.se\/auth\/callback<\/td><td>HTTPS kr\u00e4vs f\u00f6r externa dom\u00e4ner<\/td><\/tr><tr><td>Produktion<\/td><td>https:\/\/din-app.se\/auth\/callback<\/td><td>HTTPS kr\u00e4vs, inga IP-adresser<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Google skapar ett Client ID p\u00e5 formatet <code>xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com<\/code> och ett Client Secret. Kopiera b\u00e5da direkt till din <code>.env<\/code>-fil. Google-specifika endpoint-URL:er \u00e4r:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Google-specifika OAuth-endpoints f\u00f6r .env\nAUTHORIZATION_ENDPOINT=https:\/\/accounts.google.com\/o\/oauth2\/v2\/auth\nTOKEN_ENDPOINT=https:\/\/oauth2.googleapis.com\/token\nUSERINFO_ENDPOINT=https:\/\/openidconnect.googleapis.com\/v1\/userinfo\n\n# Alternativt via Google Discovery Document:\n# https:\/\/accounts.google.com\/.well-known\/openid-configuration<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En viktig detalj: Google kr\u00e4ver att din app befinner sig i &#8220;Testing&#8221;-l\u00e4get under utveckling. I testl\u00e4get kan maximalt 100 testanv\u00e4ndare logga in. F\u00f6r att \u00f6ppna appen f\u00f6r alla Google-anv\u00e4ndare m\u00e5ste du genomg\u00e5 en verifieringsprocess som kan ta 1-3 veckor f\u00f6r nya appar. Planera f\u00f6r det om du ska lansera en produkt med Google-inloggning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e4r s\u00e5 h\u00e4r:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/accounts.google.com\/o\/oauth2\/v2\/auth?\n  client_id=123456789-abc.apps.googleusercontent.com&\n  redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback&\n  response_type=code&\n  scope=openid+email+profile&\n  state=a8f3c2d9e1b4f7a0&\n  code_challenge=somerandombase64urlstring43characterslongg&\n  code_challenge_method=S256<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notera att <code>code_verifier<\/code> inte syns i URL:en alls. Den lagrades i sessionen n\u00e4r din server genererade PKCE-parametrarna. Det \u00e4r precis den egenskap som g\u00f6r PKCE s\u00e4kert: auktoriseringsservern ser aldrig verifieraren, bara challenge-hashen som den sedan j\u00e4mf\u00f6r mot verifieraren vid tokenutbytet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"keycloak-som-sjalvhostad-oauth-server\">Keycloak som sj\u00e4lvhostad OAuth-server<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Organisationer med strikta dataskyddskrav, till exempel enligt <strong>GDPR<\/strong> eller den svenska <strong>Dataskyddsf\u00f6rordningen<\/strong>, kan beh\u00f6va h\u00e5lla all autentiseringsdata inom EU. Keycloak \u00e4r en \u00f6ppen k\u00e4llkod-identitetsserver som du kan drifts\u00e4tta p\u00e5 din egen infrastruktur och som har fullt PKCE-st\u00f6d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En lokal Keycloak-instans f\u00f6r testning k\u00f6rs enkelt via Docker:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Starta Keycloak lokalt med Docker\ndocker run -d \\\n  --name keycloak \\\n  -p 8080:8080 \\\n  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \\\n  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \\\n  quay.io\/keycloak\/keycloak:latest start-dev\n\n# Keycloak Admin Console: http:\/\/localhost:8080\/admin<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I Keycloak Admin Console skapar du ett nytt &#8220;Realm&#8221; (t.ex. &#8220;demo&#8221;), l\u00e4gger till en klient med typen &#8220;OpenID Connect&#8221; och aktiverar &#8220;Standard flow&#8221; (auktoriseringskodfl\u00f6det). I klientens &#8220;Advanced&#8221;-inst\u00e4llningar aktiverar du &#8220;Proof Key for Code Exchange Code Challenge Method&#8221; och v\u00e4ljer <code>S256<\/code>. L\u00e4gg till din redirect URI i &#8220;Valid redirect URIs&#8221;-f\u00e4ltet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keycloak-endpoints f\u00f6r en lokal installation och ett realm som heter &#8220;demo&#8221; ser ut s\u00e5 h\u00e4r:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Keycloak-specifika endpoints f\u00f6r .env\nAUTHORIZATION_ENDPOINT=http:\/\/localhost:8080\/realms\/demo\/protocol\/openid-connect\/auth\nTOKEN_ENDPOINT=http:\/\/localhost:8080\/realms\/demo\/protocol\/openid-connect\/token\nUSERINFO_ENDPOINT=http:\/\/localhost:8080\/realms\/demo\/protocol\/openid-connect\/userinfo\n\n# Keycloak Discovery Document (alla endpoints automatiskt):\n# http:\/\/localhost:8080\/realms\/demo\/.well-known\/openid-configuration<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00f6rdelen med Keycloak \u00e4r full kontroll: du best\u00e4mmer vilka attribut som ing\u00e5r i tokens, kan koppla Keycloak till din befintliga LDAP\/Active Directory-katalog, och all autentiseringsdata stannar p\u00e5 din egna infrastruktur. Det g\u00f6r Keycloak till ett popul\u00e4rt val f\u00f6r svenska myndigheter, banker och h\u00e4lsov\u00e5rdsorganisationer som m\u00e5ste uppfylla specifika dataplaceringskrav.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Nackdelen \u00e4r driftkostnaden. Keycloak kr\u00e4ver minst 512 MB RAM per instans och ett relationsdatabasbackend f\u00f6r produktionsdrift. J\u00e4mf\u00f6rt med att anv\u00e4nda Google eller GitHub OAuth, d\u00e4r infrastrukturen sk\u00f6ts av leverant\u00f6ren, \u00e4r det en betydande driftskostnad att v\u00e4ga mot dataskyddsvinsten.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sakerhetshardning-av-oauth-implementationen\">S\u00e4kerhetsh\u00e4rdning av OAuth-implementationen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En fungerande OAuth 2.0 + PKCE-implementation \u00e4r ett bra fundament, men produktionsmilj\u00f6er beh\u00f6ver ytterligare h\u00e4rdning ut\u00f6ver vad som visats i k\u00e4rn-implementationen. Dessa tre \u00e5tg\u00e4rder \u00e4r de mest impactfulla:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"token-rotation-och-saker-lagring\">Token-rotation och s\u00e4ker lagring<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Refresh_token \u00e4r lika k\u00e4nsliga som l\u00f6senord och ska behandlas d\u00e4refter. I v\u00e5r implementation lagras de i Express-sessionen, som i sin tur lagras server-side (antingen i minnet eller Redis). Det \u00e4r korrekt. Men om du av n\u00e5gon anledning beh\u00f6ver lagra refresh_token i en databas, kryptera dem med AES-256 innan lagring. V\u00e5r guide om <a href=\"\/se\/aes-256-encryption-nodejs\/\">AES-256-kryptering i Node.js<\/a> visar hur du g\u00f6r det.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Aktivera refresh_token-rotation hos din leverant\u00f6r om m\u00f6jligt. Token-rotation inneb\u00e4r att varje g\u00e5ng en refresh_token anv\u00e4nds f\u00f6r att h\u00e4mta ett nytt access_token, utf\u00e4rdas ocks\u00e5 ett nytt refresh_token. Det gamla ogiltigf\u00f6rklaras. Det begr\u00e4nsar skadan om ett refresh_token l\u00e4cker: angriparen kan bara anv\u00e4nda det en g\u00e5ng innan det uppdateras.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cors-konfiguration-for-api-endpoints\">CORS-konfiguration f\u00f6r API-endpoints<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dina <code>\/api\/*<\/code>-endpoints beh\u00f6ver CORS-headers om de anropas fr\u00e5n ett annat ursprung, t.ex. fr\u00e5n en SPA p\u00e5 en annan port eller dom\u00e4n. Installera <code>cors<\/code>-paketet och konfigurera det restriktivt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install cors\n\n\/\/ src\/server.js\nconst cors = require('cors');\n\nconst allowedOrigins = [\n  'http:\/\/localhost:3000',\n  'https:\/\/din-app.se'\n];\n\napp.use('\/api', cors({\n  origin: (origin, callback) => {\n    if (!origin || allowedOrigins.includes(origin)) {\n      callback(null, true);\n    } else {\n      callback(new Error('Inte till\u00e5tet av CORS'));\n    }\n  },\n  credentials: true, \/\/ Kr\u00e4vs f\u00f6r att skicka session-cookies cross-origin\n  methods: ['GET', 'POST'],\n  allowedHeaders: ['Content-Type', 'Authorization']\n}));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">S\u00e4tt aldrig <code>origin: '*'<\/code> (wildcard) p\u00e5 endpoints som kr\u00e4ver autentisering via cookies. Wildcard CORS och <code>credentials: true<\/code> \u00e4r en ogiltig kombination som webbl\u00e4sare avvisar, och med goda sk\u00e4l: det skulle \u00f6ppna f\u00f6r cross-site request forgery p\u00e5 dina API-anrop.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"loggning-och-audit-trail\">Loggning och audit trail<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00f6r NIS2-kompatibla system och GDPR-efterlevnad beh\u00f6ver du logga autentiseringsh\u00e4ndelser utan att logga k\u00e4nslig data. Logga h\u00e4ndelserna inloggning, utloggning, misslyckad autentisering och token-f\u00f6rnyelse med tidsst\u00e4mpel och anv\u00e4ndar-ID (sub-claimet fr\u00e5n id_token, inte anv\u00e4ndarens epostadress). Under NIS2-direktivet, som implementerades i Sverige via Cybers\u00e4kerhetslagen 2025:1506 och tr\u00e4dde i kraft den 15 januari 2026, kr\u00e4vs att organisationer i 18 kritiska sektorer kan demonstrera loggning av autentiseringsh\u00e4ndelser som en del av sin incidentberedskap.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Audit-logging f\u00f6r autentiseringsh\u00e4ndelser (logga inte tokens)\nfunction auditLog(event, userId, metadata = {}) {\n  const entry = {\n    timestamp: new Date().toISOString(),\n    event: event,\n    userId: userId,\n    ip: metadata.ip,\n    userAgent: metadata.userAgent?.substring(0, 100)\n  };\n  console.log(JSON.stringify(entry));\n  \/\/ I produktion: skicka till din log-aggregator (ELK, Splunk, osv.)\n}\n\n\/\/ I auth-routern efter lyckad inloggning:\nauditLog('oauth_login_success', req.session.user.sub, {\n  ip: req.ip,\n  userAgent: req.headers['user-agent']\n});\n\n\/\/ Vid misslyckad state-validering:\nauditLog('oauth_csrf_attempt', 'unknown', {\n  ip: req.ip,\n  receivedState: state?.substring(0, 8) + '...'\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 med PKCE \u00e4r inte bara ett tekniskt protokoll, det \u00e4r en grundl\u00e4ggande byggsten i varje modern webbapplikations s\u00e4kerhetsarkitektur. Implementationen i den h\u00e4r guiden ger dig ett komplett, produktionsn\u00e4ra OAuth-fl\u00f6de som uppfyller kraven i RFC 7636, RFC 9700, och de kommande OAuth 2.1-standarderna. Kombinerat med de relaterade implementationsguiderna f\u00f6r <a href=\"\/se\/jwt-authentication-nodejs\/\">JWT<\/a>, <a href=\"\/se\/two-factor-authentication-nodejs\/\">tv\u00e5faktorsautentisering<\/a> och <a href=\"\/se\/nodejs-session-management\/\">sessionshantering<\/a> har du fundamentet f\u00f6r ett s\u00e4kert autentiseringssystem som h\u00e5ller 2026 och fram\u00e5t.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>OAuth 2.0 med PKCE (Proof Key for Code Exchange) \u00e4r 2026 \u00e5rs standardmetod f\u00f6r s\u00e4ker auktorisering i webbapplikationer. Den gamla implicita fl\u00f6det f\u00f6rsvann ur OAuth 2.0-specifikationen (RFC 6749) efter att\u2026<\/p>\n","protected":false},"author":8,"featured_media":114,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-113","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-10","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/posts\/113","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/users\/8"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/comments?post=113"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/posts\/113\/revisions"}],"predecessor-version":[{"id":115,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/posts\/113\/revisions\/115"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/media\/114"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/media?parent=113"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/categories?post=113"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/se\/wp-json\/wp\/v2\/tags?post=113"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}