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ödestypSäkerhetsnivåStatus 2026Klienttyp
Implicit FlowLåg (token i URL)Föråldrat, borttaget i OAuth 2.1Public clients
Authorization CodeMedel (utan PKCE)Acceptabelt för konfidentiella klienterConfidential clients
Authorization Code + PKCEHögRekommenderat för alla klienttyperAlla
Client CredentialsHög (maskin-till-maskin)AktivtConfidential clients
Device CodeMedelAktivt för IoT och CLI-apparEnhetsbegrä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:

KravVersion / detaljSyfte i projektet
Node.js22.x LTS (minst 18.x)Runtime med inbyggd crypto-modul
npm10.xPakethantering och skriptköring
Express4.21.xHTTP-server och routing
express-session1.18.xSessionslagring server-side
axios1.7.xHTTP-klient för token-endpoint
dotenv16.xMiljövariabelhantering
OAuth 2.0-leverantörGoogle, GitHub, Okta eller KeycloakAuktoriseringsserver
Grundläggande Express-kunskapRouting, middlewareFö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:

StegAktörÅtgärdData som skickasData som sparas
1BrowserGET /auth/loginSession-cookieIngenting
2Node.js-serverGenererar verifier, challenge och stateIngenting externtcode_verifier, oauthState i session
3Node.js-serverRedirect till auktoriseringsserverclient_id, code_challenge, S256, state, scopeIngenting nytt
4AnvändareLoggar in och godkännerInloggningsuppgifter till auktoriseringsservernHos auktoriseringsservern
5AuktoriseringsserverRedirect till /auth/callbackcode (engångsbruk), stateIngenting på klienten
6Node.js-serverVerifierar state, hämtar code_verifier ur sessionIntern sessionslookupIngenting nytt
7Node.js-serverPOST till token_endpointcode, code_verifier, client_id, redirect_uriIngenting ännu
8AuktoriseringsserverVerifierar SHA256(verifier) == challengeReturnerar access_token, refresh_tokenHos auktoriseringsservern
9Node.js-serverGET userinfo med access_tokenBearer-token i headeruser, accessToken, refreshToken, tokenExpiry i session
10Node.js-serverRensar PKCE-data, redirect till /Session-cookie till browsercode_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:

FelmeddelandeHTTP-kodVanligaste orsakLösning
invalid_grant400code_verifier matchar inte challenge, eller koden har redan använtsVerifiera att SHA256(verifier) ger rätt challenge. Auktoriseringskoder är engångsbruk.
invalid_request400Redirect URI ej registrerad, eller saknad PKCE-parameterKontrollera att redirect_uri matchar exakt vad du registrerat hos leverantören, tecken för tecken.
invalid_client401Fel client_id eller client_secretKontrollera .env-värdena. Koda aldrig in dem direkt i källkoden.
State mismatch (din 403)403Sessionen gick ut mellan login och callback, eller cookies blockerasKontrollera att sameSite=’lax’ är satt och att sessionen lever länge nog för inloggningsflödet.
CORS-fel vid token-endpointWebbläsarfelToken-request görs direkt från frontend-JavaScriptToken-request MÅSTE göras server-side. Flytta axios.post-anropet till Node.js-backend.
access_denied400Användaren nekade åtkomst, eller scope saknas i leverantörens konfigurationKontrollera att begärda scopes är aktiverade i leverantörens app-konfiguration.
Cannot read properties of undefined (codeVerifier)500Session initialiserades inte, eller express-session saknasVerifiera att app.use(session(…)) körs INNAN routing-middleware i server.js.
redirect_uri_mismatch400URI i koden matchar inte registrerad URI hos leverantörenURI: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

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 registreraKommentar
Lokal utvecklinghttp://localhost:3000/auth/callbackHTTP tillåtet för localhost
Staginghttps://staging.din-app.se/auth/callbackHTTPS krävs för externa domäner
Produktionhttps://din-app.se/auth/callbackHTTPS 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.