OAuth 2.0 og OpenID Connect (OIDC) er rygraden i moderne webautentificering. Mere end 90 % af alle nye webapplikationer bruger disse protokoller i 2026, men fejlagtig implementering er den næstmest udbredte årsag til autentificeringsbrud ifølge OWASPs Top 10 2025-rapport. Denne guide leder dig igennem 12 præcise trin til et fuldstændigt og sikkert OIDC-login med Node.js, Express og openid-client v6.8.4, den eneste OpenID Certified™ OAuth 2 / OpenID Connect-klient for JavaScript-kørselsmiljøer. Du bygger en komplet applikation med PKCE, token-validering og beskyttede ruter på under 30 minutter.

Hvad er OAuth 2.0 og OpenID Connect?

OAuth 2.0 er en autorisationsprotokol, defineret i RFC 6749 fra 2012. Den giver tredjepartsapplikationer adgang til beskyttede ressourcer på vegne af en bruger uden at kende brugerens kodeord. En OAuth 2.0-server udsteder kortvarige access tokens, som applikationen bruger til at kalde API’er. OAuth 2.0 handler udelukkende om delegeret autorisation, ikke om hvem brugeren er.

OpenID Connect (OIDC) løser det manglende led: identitet. OIDC er et identitetslag oven på OAuth 2.0, specificeret af OpenID Foundation. Det tilføjer et ID-token, et JSON Web Token (JWT), der indeholder verificerede oplysninger om brugeren: bruger-ID, e-mail, navn og tidsstempel for autentificering. OIDC er det, der giver dig “Log ind med Google” og “Log ind med Microsoft” på millioner af hjemmesider.

I praksis bruger du altid OIDC, når du vil autentificere brugere. Du bruger rent OAuth 2.0, når du kun har brug for adgang til en API, f.eks. at læse en brugers Google Calendar-begivenheder, uden at logge dem ind på dit eget system. Forskellen er afgørende for at vælge den rigtige flow og de rigtige scopes.

OAuth 2.1, den kommende opdatering til standarden, fjerner officielt Implicit Flow og Resource Owner Password Credentials Grant. Begge flows betragtes som usikre i 2026. Authorization Code Flow med PKCE er det eneste anbefalede flow for webapplikationer fremadrettet, og det er præcis det, du implementerer her.

OAuth 2.0 mod OpenID Connect: Kerneforskellene

Mange udviklere bruger OAuth 2.0 og OIDC i flæng, men de løser fundamentalt forskellige problemer. Tabellen nedenfor viser de tre mest brugte autentificerings- og autorisationsprotokoller side om side.

ProtokolFormålToken-typePrimær use caseUdstedt af
OAuth 2.0Delegeret autorisationAccess Token (opaque eller JWT)API-adgang på vegne af brugerAuthorization Server
OpenID ConnectAutentificering + autorisationID-token (JWT) + Access TokenBrugerlogin, SSOIdentity Provider (IdP)
SAML 2.0Enterprise SSOXML-assertionVirksomhedslogin, legacy-systemerIdentity Provider

OAuth 2.0 Access Tokens indeholder typisk kun en bruger-ID og udløbstid. ID-tokens fra OIDC indeholder derimod verifikation af autentificeringstidspunkt (auth_time), nonce til replay-beskyttelse og brugeroplysninger (claims) som email, name og sub (subject identifier). Brug altid ID-tokenet til at fastslå brugerens identitet, og brug access tokenet til at kalde beskyttede API-endepunkter.

En kritisk fejl, mange udviklere begår, er at validere en brugers identitet ved at bruge access tokenet. Access tokens er til ressourceservere, ikke til din applikation. Brug ID-tokenet til login, og valider altid signaturen, udløbstiden og audience-claimet (aud) i ID-tokenet.

Authorization Code Flow med PKCE: Standardflowet i 2026

Authorization Code Flow med PKCE (Proof Key for Code Exchange, RFC 7636) er det eneste anbefalede flow for webapplikationer og mobile apps i 2026. Flowet forhindrer authorization code interception attacks, en angrebstype, hvor en ondsindet app på enheden opsnapper autoriseringskoden, før den når din applikation.

Sekvensen ser sådan ud i praksis: Din applikation genererer en tilfældig code_verifier (minimum 43 tegn). Den beregner derefter en code_challenge ved at SHA-256-hashe verifikatoren og base64url-encode resultatet. Autoriseringskoden, der sendes til identity provideren, indeholder kun udfordringen, ikke verifikatoren. Når autoriseringskoden returneres til din callback-URL, sender du verifikatoren i token-anmodningen. Identity provideren matcher udfordringen med den hashede verifikator og bekræfter dermed, at token-anmodningen kommer fra den applikation, der startede flowen.

PKCE kombineres med state-parameteren til CSRF-beskyttelse og nonce-parameteren til replay-angrebsbeskyttelse. Alle tre parametre er obligatoriske i en sikker implementation. openid-client v6 håndterer automatisk generering og validering af alle tre, hvis du bruger bibliotekets hjælpefunktioner.

Forudsætninger

Inden du begynder, skal følgende software og konti være klar.

KravVersion / detaljerFormål
Node.jsv22.0+ (LTS anbefales)JavaScript-kørselsmiljø
npmv10.0+Pakkehåndtering
openid-clientv6.8.4OIDC-klient (OpenID Certified™)
expressv4.21+HTTP-framework
express-sessionv1.19.0Server-side session
dotenvv16.xMiljøvariabelhåndtering
Identity Provider-kontoOkta (gratis udviklerkonto) eller Auth0OIDC-server
Grundlæggende Node.js-kendskabExpress, async/awaitForudsætning

Kontrollér din Node.js-version med node --version. Kørselsmiljøer ældre end Node.js v18 understøtter ikke Web Crypto API, som openid-client v6 bruger internt til kryptografiske operationer. Node.js v22 LTS er stærkt anbefalet.

openid-client v6 er et rent ES-modul (ESM). Du kan enten bruge type: "module" i din package.json eller importere det dynamisk med await import() i CommonJS-filer. Denne guide bruger ES-moduler fra start.

Trin 1: Opret projektet og installér afhængigheder

Start med at oprette en ny projektmappe og initialisere et Node.js-projekt. Brug --yes-flaget til at springe det interaktive setup over.

mkdir oidc-demo && cd oidc-demo
npm init --yes
npm install express openid-client express-session dotenv

Tilføj "type": "module" til din package.json for at aktivere ES-moduler. Åbn filen og opdater den:

{
  "name": "oidc-demo",
  "version": "1.0.0",
  "type": "module",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "node --watch app.js"
  },
  "dependencies": {
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "express-session": "^1.19.0",
    "openid-client": "^6.8.4"
  }
}

Projektstrukturen ser sådan ud, når du er færdig:

oidc-demo/
├── .env              # Miljøvariabler (aldrig i versionskontrol)
├── .env.example      # Skabelon til andre udviklere
├── .gitignore        # Ekskluderer .env og node_modules
├── app.js            # Hovedapplikation
├── auth.js           # OIDC-klient og hjælpefunktioner
├── middleware.js     # Middleware til beskyttede ruter
└── package.json

Opret en .gitignore-fil med det samme for at sikre, at hemmeligheder ikke ender i versionskontrol:

node_modules/
.env
*.log

Trin 2-3: Konfigurer Identity Provider og miljøvariabler

Denne guide bruger Okta som Identity Provider, men trinene er næsten identiske for Auth0, Keycloak og andre OIDC-kompatible udbydere. Opret en gratis Okta Developer-konto på developer.okta.com og følg disse trin:

Trin 2: Konfigurer applikationen i Okta

Log ind i Okta-administrationspanelet og naviger til Applications > Applications. Klik på Create App Integration og vælg OIDC – OpenID Connect som login-metode og Web Application som applikationstype. Udfyld følgende felter:

Appintegrationsnavn: OIDC Demo Node.js. Sign-in redirect URIs: http://localhost:3000/callback. Sign-out redirect URIs: http://localhost:3000. Klik på Save. Okta viser nu dit Client ID og Client Secret. Gem disse to værdier straks.

Find din Okta-domæne-URL øverst til højre i administrationspanelet under dit kontonavn (formatet er https://dev-xxxxxxx.okta.com). Tilføj /oauth2/default for at få den fulde issuer-URL: https://dev-xxxxxxx.okta.com/oauth2/default.

Under Advanced Settings i din applikationskonfiguration, sørg for:

Token Endpoint Authentication Method: sæt til client_secret_basic. Dette er den sikreste metode til server-side applikationer, da klienthemmeligheden sendes i HTTP Authorization-headeren, base64-kodet, og aldrig eksponeres i URL’en.

Trin 3: Opret .env-filen

Opret en .env-fil i projektets rodmappe med de værdier, du netop hentede fra Okta. Erstat eksempleværdierne med dine faktiske Okta-legitimationsoplysninger.

# .env - ALDRIG commit denne fil til versionskontrol
ISSUER_URL=https://dev-xxxxxxx.okta.com/oauth2/default
CLIENT_ID=0oa1234567890abcdef
CLIENT_SECRET=din-hemmelige-noegle-her
REDIRECT_URI=http://localhost:3000/callback
SESSION_SECRET=et-langt-tilfaeldigt-hemmeligt-ord-minimum-32-tegn
PORT=3000

Opret også en .env.example-fil med tomme værdier, som du kan inkludere i versionskontrol som dokumentation for andre udviklere. Gentag aldrig klienthemmeligheder, API-nøgler eller session-hemmeligheder i kildekoden. Disse oplysninger giver en angriber fuld adgang til at udgive sig som din applikation.

Trin 4-5: Express og session-middleware

Opret den centrale app.js-fil. Session-konfigurationen er kritisk for sikkerheden: du gemmer temporære PKCE-parametre og brugeroplysninger i serveren, ikke i cookies alene.

// app.js
import 'dotenv/config';
import express from 'express';
import session from 'express-session';
import { login, callback, logout, getOidcConfig } from './auth.js';
import { requireAuth } from './middleware.js';

const app = express();

// Sikkerheds-headers
app.set('trust proxy', 1); // Påkrævet bag en reverse proxy i produktion

// Session-middleware: gemmer PKCE-parametre server-side
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,         // Forhindrer JavaScript-adgang til cookie
    secure: process.env.NODE_ENV === 'production', // HTTPS-only i produktion
    sameSite: 'lax',        // CSRF-beskyttelse
    maxAge: 60 * 60 * 1000 // 1 time i millisekunder
  }
}));

// Initialiser OIDC-klienten ved opstart
await getOidcConfig();

// Ruter
app.get('/login', login);
app.get('/callback', callback);
app.get('/logout', logout);

// Beskyttet rute: requireAuth middleware verificerer session
app.get('/profile', requireAuth, (req, res) => {
  const user = req.session.user;
  res.json({
    sub: user.sub,
    email: user.email,
    name: user.name,
    loggedInAt: new Date(user.auth_time * 1000).toISOString()
  });
});

// Offentlig rute
app.get('/', (req, res) => {
  const isLoggedIn = !!req.session.user;
  res.send(`
    

OIDC Demo

${isLoggedIn ? `

Logget ind som ${req.session.user.email}

Se profil | Log ud` : `Log ind med Okta` } `); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server kører på http://localhost:${port}`); });

Cookie-indstillingerne httpOnly: true og sameSite: 'lax' er ikke valgfrie. httpOnly forhindrer JavaScript på klientsiden i at læse session-cookien, hvilket eliminerer risikoen for, at XSS-angreb stjæler sessioner. sameSite: 'lax' tillader cookies at følge med ved normale navigationer (klik på link), men blokerer dem i cross-site POST-anmodninger, der bruges i CSRF-angreb.

secure: true i produktion sikrer, at session-cookien kun sendes over HTTPS-forbindelser. I udviklingsmiljøet sættes dette til false via NODE_ENV-variablen, da localhost typisk kører over HTTP.

Trin 6-7: OIDC Discovery og login-flow med PKCE

Opret auth.js-filen med den fulde OIDC-implementation. Brug client.discovery() til automatisk at hente alle nødvendige endepunkts-URL’er fra Identity Providerens .well-known/openid-configuration-dokument.

// auth.js
import * as client from 'openid-client';

let oidcConfig;

// Trin 6: OIDC Discovery - hent konfiguration fra Identity Provider
export async function getOidcConfig() {
  if (oidcConfig) return oidcConfig;

  oidcConfig = await client.discovery(
    new URL(process.env.ISSUER_URL),
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET
  );

  console.log('OIDC Discovery gennemført:', process.env.ISSUER_URL);
  return oidcConfig;
}

// Trin 7: Start login-flow med PKCE
export async function login(req, res) {
  const config = await getOidcConfig();

  // Generer PKCE-parametre og sikkerhedsparametre
  const codeVerifier = client.randomPKCECodeVerifier();
  const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
  const state = client.randomState();
  const nonce = client.randomNonce();

  // Gem parametre i server-side session (ikke i cookie)
  req.session.codeVerifier = codeVerifier;
  req.session.state = state;
  req.session.nonce = nonce;

  // Byg autoriserings-URL
  const authorizationUrl = client.buildAuthorizationUrl(config, {
    redirect_uri: process.env.REDIRECT_URI,
    scope: 'openid profile email',
    response_type: 'code',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state,
    nonce,
  });

  // Omdiriger brugeren til Identity Provider
  res.redirect(authorizationUrl.href);
}

client.randomPKCECodeVerifier() genererer en kryptografisk stærk tilfældig streng på 43-128 tegn ved brug af Web Crypto API. client.calculatePKCECodeChallenge() hasher verifikatoren med SHA-256 og base64url-encoder resultatet. code_challenge_method: 'S256' specificerer, at SHA-256-metoden bruges, i modsætning til den svagere og forældede plain-metode.

PKCE-parametrene gemmes i serveren via Express-sessionen, ikke i URL-parametre eller cookies. Klientens browser modtager kun et session-ID i en httpOnly-cookie. Denne adskillelse sikrer, at angribere ikke kan rekonstruere autorisationsflows, selv hvis de kan læse cookies.

Trin 8-9: Callback-handler og token-validering

Callback-handleren modtager autoriseringskoden fra Identity Provideren og udveksler den til tokens. openid-client validerer automatisk ID-tokenets signatur, udløbstid, issuer, audience, nonce og state.

// Fortsættelse af auth.js

// Trin 8: Callback-handler - modtag autoriseringskode og udveksl til tokens
export async function callback(req, res) {
  const config = await getOidcConfig();

  try {
    // Rekonstruer den aktuelle URL fra anmodningen
    const currentUrl = new URL(
      req.url,
      `${req.protocol}://${req.hostname}:${process.env.PORT}`
    );

    // Udveksl autoriseringskode til tokens (validerer automatisk state og PKCE)
    const tokens = await client.authorizationCodeGrant(config, currentUrl, {
      pkceCodeVerifier: req.session.codeVerifier,   // PKCE-validering
      expectedState: req.session.state,              // CSRF-beskyttelse
      expectedNonce: req.session.nonce,              // Replay-beskyttelse
    });

    // Trin 9: Hent og gem brugeroplysninger fra ID-token
    const claims = tokens.claims();

    // Gem brugeroplysninger i session
    req.session.user = {
      sub: claims.sub,           // Unik bruger-ID hos Identity Provider
      email: claims.email,
      name: claims.name,
      auth_time: claims.auth_time,
      accessToken: tokens.access_token,
    };

    // Ryd PKCE-parametre fra session (de er kun nødvendige én gang)
    delete req.session.codeVerifier;
    delete req.session.state;
    delete req.session.nonce;

    // Omdiriger til beskyttet side
    res.redirect('/profile');

  } catch (err) {
    console.error('OIDC callback-fejl:', err.message);
    res.status(400).send(`Autentificeringsfejl: ${err.message}`);
  }
}

// Trin 11: Logout
export async function logout(req, res) {
  const config = await getOidcConfig();
  const idToken = req.session.user?.idToken;

  // Ødelæg den lokale session
  req.session.destroy((err) => {
    if (err) console.error('Session-destroy fejl:', err);

    // Byg end-session URL hos Identity Provider
    const logoutUrl = client.buildEndSessionUrl(config, {
      post_logout_redirect_uri: `${req.protocol}://${req.hostname}:${process.env.PORT}`,
      id_token_hint: idToken,
    });

    res.redirect(logoutUrl?.href || '/');
  });
}

client.authorizationCodeGrant() udfører automatisk følgende verifikationstrin: den henter tokens fra token-endepunktet med client_secret_basic-autentificering, verificerer ID-tokenets JWT-signatur mod Identity Providerens offentlige nøgler, kontrollerer iss-claimet mod den forventede issuer, kontrollerer aud-claimet mod dit Client ID, kontrollerer exp-claimet og sikrer, at tokenet ikke er udløbet, verificerer nonce-claimet mod den gemte nonce for at forhindre replay-angreb, og verificerer state-parameteren mod den gemte state for at forhindre CSRF.

Disse verifikationstrin sker på én kodelinje. En manuel implementation af blot ét af disse trin tager op til en times arbejde og er fyldt med potentielle fejl. Det er præcis grunden til, at en OpenID Certified™-klientimplementation er afgørende for produktionskode.

Trin 10: Middleware til beskyttede ruter

Opret middleware.js med et enkelt middleware, der verificerer, om brugeren er autentificeret, og omdirigerer til login-siden, hvis ikke.

// middleware.js

// Middleware til beskyttelse af ruter
export function requireAuth(req, res, next) {
  if (!req.session.user) {
    // Gem den ønskede URL i session, så brugeren returneres efter login
    req.session.returnTo = req.originalUrl;
    return res.redirect('/login');
  }
  next();
}

// Middleware til at injicere brugeroplysninger i alle skabeloner
export function injectUser(req, res, next) {
  res.locals.user = req.session.user || null;
  next();
}

Brug requireAuth som middleware på alle ruter, der kræver login. I app.js sættes det direkte i route-definitionen: app.get('/dashboard', requireAuth, dashboardController). Middlewaret er genbrugeligt på tværs af alle beskyttede endpoints og kræver ikke duplikering af autentificeringskontrollogik.

Session-opslaget sker i hukommelsen som standard i Express. I produktion skal du erstatte den in-memory session-store med en vedvarende butik som Redis (connect-redis-pakken) eller PostgreSQL. In-memory sessions mistes ved genstart af applikationen, og de skalerer ikke til flere Node.js-instanser.

Trin 11-12: Test applikationen og se output

Start applikationen og test hele login-flowet:

node app.js
# Output:
# OIDC Discovery gennemført: https://dev-xxxxxxx.okta.com/oauth2/default
# Server kører på http://localhost:3000

Åbn http://localhost:3000 i din browser. Du ser hjemmesiden med linket “Log ind med Okta”. Klik på linket og gennemfør Okta-login-flowet. Efter succesfuld autentificering omdirigeres du til /profile, hvor du ser en JSON-respons som denne:

{
  "sub": "00u1a2b3c4d5e6f7g8h9",
  "email": "[email protected]",
  "name": "Søren Hansen",
  "loggedInAt": "2026-06-17T10:30:45.000Z"
}

Kontrollér netværkstrafikken i browserens udviklingsværktøjer under login-flowet. Du kan se tre HTTP-anmodninger: 1) GET /login, som omdirigerer til Oktas autorisations-URL med code_challenge-parameteren, 2) GET /callback?code=...&state=..., som modtager autoriseringskoden fra Okta, og 3) en POST-anmodning fra din server til Oktas token-endepunkt, som kun er synlig i serverloggen.

Verificer PKCE-sikkerheden ved at forsøge at besøge /callback direkte uden en aktiv session med PKCE-parametrene gemt. Applikationen svarer med en 400-fejl: “OIDC callback-fejl: invalid_grant” eller lignende, fordi code_verifier-parameteren mangler i session.

Fem kritiske faldgruber ved OAuth 2.0 og OIDC

Disse fejl dukker op i produktionskode igen og igen. Ingen af dem er åbenlyse for en ny udvikler, og alle kan føre til alvorlige sikkerhedsbrud.

Faldgrube 1: Udelader state-parameteren

State-parameteren beskytter mod CSRF-angreb i OAuth-flowet. Uden den kan en angriber lokke en bruger til at gennemføre et login-flow, der binder brugerens konto til angriberens session (login CSRF). openid-client v6 genererer state automatisk via client.randomState(), men det er dit ansvar at gemme og verificere den i session. Aldrig spring denne validering over, selv i prototyper.

Faldgrube 2: Bruger Implicit Flow i 2026

Implicit Flow returnerer tokens direkte i URL-fragmentet (#access_token=...). Tokens i URL’er ender i browserhistorik, serverlogfiler og Referer-headers. OAuth 2.1 fjerner officielt Implicit Flow. Brug altid Authorization Code Flow med PKCE, aldrig Implicit Flow, selv til SPA’er.

Faldgrube 3: Gemmer access tokens i localStorage

localStorage er tilgængeligt via JavaScript, hvilket gør alle gemte tokens sårbare over for XSS-angreb. Et XSS-angreb på ét sted på din side stjæler alle tokens fra localStorage. Gem tokens server-side i sessions, og brug httpOnly-cookies til session-ID’et. Browsersidede SPA’er uden backend bør bruge token-rotation og BFF-mønsteret (Backend For Frontend).

Faldgrube 4: Ingen nonce-validering

Nonce forhindrer replay-angreb, hvor en angriber genbruger et afsnappet ID-token til at starte en ny session. Generer en tilfældig nonce ved hvert login-forsøg med client.randomNonce(), gem den i session og konfigurer expectedNonce i authorizationCodeGrant()-kaldet. openid-client verificerer automatisk nonce-claimet i ID-tokenet.

Faldgrube 5: Validerer ikke redirect_uri præcist

En åben redirect-sårbarhed opstår, hvis Identity Provideren accepterer redirect URIs, der delvist matcher. Sørg for, at din Identity Provider kun tillader din præcise redirect URI, inklusiv protokol, port og sti. http://localhost:3000/callback og http://localhost:3000/callback/ (med trailing slash) er to forskellige URI’er og bør ikke begge accepteres i produktion. Konfigurér altid redirect URIs eksplicit i Identity Providerens administrationspanel.

Fejlfinding: 10 hyppige problemer og løsninger

Disse fejlmeddelelser og problemer opstår næsten uundgåeligt, første gang du implementerer OIDC. Her er årsagerne og løsningerne.

Fejlmeddelelse / SymptomSandsynlig årsagLøsning
invalid_grantAutoriseringskoden er allerede brugt eller udløbetAutoriseringskoder er engangsbrug og udløber typisk efter 5 minutter. Sørg for, at callback-handleren kun kalder authorizationCodeGrant() én gang.
invalid_clientClient ID eller Client Secret er forkertKontrollér .env-filen og sammenlign med Identity Provider-konfigurationen. Sørg for, at der ikke er mellemrum eller linjeskift i miljøvariablerne.
redirect_uri_mismatchRedirect URI i anmodningen matcher ikke den konfigurerede URISørg for, at REDIRECT_URI i .env præcis matcher den URI, du har konfigureret i Identity Providerens administrationspanel, tegn for tegn.
invalid_nonceNonce-claimet i ID-tokenet matcher ikke den gemte nonceSessionen er muligvis udløbet mellem login og callback. Øg session-timeout eller implementer nonce-persistering i database.
iss claim mismatchIssuer-URL i ID-tokenet matcher ikke konfigurationenKontrollér ISSUER_URL i .env. Okta-issuer inkluderer typisk /oauth2/default. Brug præcis den URL, der vises i .well-known/openid-configuration.
Bruger sendes i loop til loginSession gemmes ikke korrektTjek at saveUninitialized: false og resave: false er sat. Verificer at session-hemmeligheden er en lang, tilfældig streng.
TypeError: Cannot read property 'discovery' of undefinedopenid-client er importeret forkertSørg for at bruge import * as client from 'openid-client' og at package.json indeholder "type": "module".
CORS-fejl i browserSPA forsøger at kalde token-endepunkt direkteToken-udvekslingen skal altid ske server-side. Brug BFF-mønsteret. Frontend kalder aldrig token-endepunktet direkte.
jwks_uri kan ikke hentesFirewallregel blokerer udgående HTTP til Identity ProviderTillad udgående HTTPS-trafik til Identity Providerens domæne. For Okta: *.okta.com:443.
Session går tabt ved genstartIn-memory session-storeInstallér og konfigurér connect-redis med en Redis-instans. In-memory sessions er kun til udvikling.

Debugging-tip: aktivér detaljeret OIDC-logning ved at sætte NODE_DEBUG=openid-client node app.js i terminalen. Dette viser alle HTTP-anmodninger og -svar til og fra Identity Provideren, inklusiv den rå token-respons og verifikationstrin.

Avancerede teknikker til produktionssystemer

Token-refresh med refresh tokens

Access tokens udløber typisk efter 1 time. For at undgå at brugere logges ud, implementer token-refresh i baggrunden. Anmod om offline_access-scopet ved login for at modtage et refresh token. Gem refresh tokenet sikkert i databasen, krypteret med AES-256. Implementer en baggrundsjob, der refresher access tokens 5 minutter inden udløb ved at kalde token-endepunktet med grant_type=refresh_token.

Multiple Identity Providers

Mange produktionsapplikationer understøtter både “Log ind med Google” og “Log ind med Microsoft”. Kald client.discovery() for begge providerUrl’er ved applikationsstart og cache konfigurationsobjekterne. Brug en provider-parameter i login-URL’en (/login?provider=google) til at vælge den korrekte OIDC-konfiguration. Normaliser claims på tværs af providere, da Google bruger email_verified, mens Microsoft bruger verified_primary_email.

Enterprise SSO med SAML via OIDC-bro

Mange danske virksomheder bruger Active Directory Federation Services (AD FS) eller Azure AD som Identity Provider. Okta og Auth0 fungerer som en OIDC-til-SAML-bro: din Node.js-applikation taler OIDC med Okta, og Okta oversætter til SAML mod virksomhedens Active Directory. Du behøver ikke implementere SAML direkte i din applikation. Denne arkitektur er den anbefalede tilgang for B2B SaaS-applikationer, der skal understøtte enterprise-kunder med eksisterende AD-infrastruktur.

Userinfo-endepunkt for opdaterede claims

ID-tokenet bages ved login og afspejler ikke ændringer i brugerens oplysninger siden da. Kald userinfo-endepunktet for at hente opdaterede claims:

// Hent opdaterede brugeroplysninger fra userinfo-endepunkt
const userinfo = await client.fetchUserInfo(
  config,
  req.session.user.accessToken,
  req.session.user.sub
);
// userinfo indeholder de nyeste claims fra Identity Provider

Kald userinfo-endepunktet ved session-fornyelse, ikke ved hvert sideopkald. Cach resultatet i session med en TTL på 15 minutter for at undgå unødigt API-kald til Identity Provideren.

Sikkerhedshærdning til produktion

En funktionerende OIDC-implementation er startpunktet, ikke slutmålet. Disse trin er nødvendige inden produktionsudrulning.

HTTPS er ikke valgfrit. OIDC-flows over HTTP eksponerer tokens i klar tekst. Konfigurér din webserver (Nginx, Caddy, Apache) med TLS-certifikat via Let’s Encrypt. Sæt secure: true i session-cookie-konfigurationen og aktiver HSTS-headeren (Strict-Transport-Security: max-age=31536000; includeSubDomains).

Content Security Policy. Tilsæt Helmet.js som middleware for at sætte sikkerhedsheaders automatisk:

npm install helmet
import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      connectSrc: ["'self'", process.env.ISSUER_URL],
      frameSrc: ["'none'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
  },
}));

Session-hemmelighed i nøglestyring. SESSION_SECRET skal være minimum 32 tilfældige bytes genereret med crypto.randomBytes(32).toString('hex'). Rotér hemmeligheden regelmæssigt, og brug en nøglestyringsservice som HashiCorp Vault, AWS Secrets Manager eller Azure Key Vault i produktion. Hård-kodede hemmeligheder er den mest udbredte kilde til legitimationsoplysningslæk i Node.js-applikationer.

Rate limiting på login-endepunktet. Login-flowet starter en omdirigering til Identity Provideren, men et massivt antal anmodninger kan overbelaste din session-store. Kombinér din OIDC-implementation med rate limiting:

Se Rate Limiting i Node.js: 12 trin, 30 min for en komplet implementation med express-rate-limit.

Audit-logning. Log alle autentificeringshændelser: succesfulde logins, mislykkede callbacks og logout-hændelser. Inkluder tidsstempel, bruger-sub, IP-adresse og user-agent. Disse logs er uundværlige ved hændelseshåndtering og kræves af NIS2-direktivet for virksomheder i Danmark, der er underlagt forordningen.

Token-livstider og session-strategi: Hvad du skal vide

Korrekte token-livstider er afgørende for balance mellem sikkerhed og brugeroplevelse. Korte token-livstider begrænser skaden, hvis et token kompromitteres, men kræver hyppigere refresh. Lange livstider er mere bekvemme, men øger risikoen. Tabellen nedenfor viser typiske anbefalede token-livstider for produktionsapplikationer.

Token-typeAnbefalet livstidFormålOpbevaring
Access Token15-60 minutterAPI-kald til ressourceservereHukommelse eller server-session
ID Token15-60 minutterBrugeridentifikation ved loginServer-session (valideres ved brug)
Refresh Token7-30 dage (med rotation)Forny access tokens automatiskKrypteret i database, aldrig i browser
Autoriseringskode1-5 minutterEngangsbrug: kode-til-token-udvekslingSendes direkte til callback, gemmes ikke
Server-side session1-8 timer (idle timeout)Brugerens loginnede tilstand i appRedis eller lignende session-store

Implementér refresh token rotation for at minimere risikoen ved stjålne refresh tokens. Når et refresh token bruges til at hente et nyt access token, udsteder Identity Provideren et nyt refresh token og invaliderer det gamle. Hvis en angriber har stjålet et refresh token og forsøger at bruge det, efter det er roteret, nægtes adgang. Okta og Auth0 understøtter begge refresh token rotation som en konfigurationsindstilling i applikationen.

Implementér altid idle session timeout ud over token-livstider. En bruger, der efterlader sin browser åben på et offentligt netværk, bør logges ud efter en periode med inaktivitet, uanset om tokens stadig er gyldige. Express-session understøtter rolling sessions via rolling: true, som forny session-cookien ved hvert request og dermed implementerer idle timeout automatisk.

OIDC i danske virksomheder: Compliance og GDPR-hensyn

Danske virksomheder, der bruger OIDC-baseret login, skal tage stilling til en række GDPR-aspekter. Identity Providere som Okta og Auth0 behandler personoplysninger på dine vegne og er dermed databehandlere. Du skal indgå en databehandleraftale (DPA) med din Identity Provider, og du skal sikre, at personoplysninger kun behandles i EU/EØS-datacentre, med mindre du har et lovligt grundlag for tredjelandsoverførsler.

Okta tilbyder dataopbevaring i EU via sin EU-datahostingregion (EU Cell). Auth0 har ligeledes EU-baserede lejere. Keycloak er et open source-alternativ, du selv kan hoste på servere i Danmark eller EU, og det giver fuld kontrol over, hvor personoplysninger opbevares.

NIS2-direktivet, som trådte i kraft i Danmark i 2024, stiller krav til autentificeringssikkerhed for virksomheder inden for kritisk infrastruktur. OIDC med PKCE og to-faktor-autentificering opfylder direktivets krav til stærk autentificering. Ifølge Center for Cybersikkerhed (CFCS) var kun 16 % af de virksomheder, der er underlagt NIS2 i Danmark, fuldt compliant ved udgangen af 2025.

For virksomheder, der håndterer særligt følsomme personoplysninger, bør du overveje at kombinere OIDC med to-faktor-autentificering. OIDC understøtter Authentication Context Class Reference (ACR) claims, som specificerer det autentificeringsniveau, der kræves. Et ACR-claim på urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract kræver, at brugeren har gennemført 2FA, ikke kun kodeordslogin. Din Node.js-applikation kan verificere dette claim i ID-tokenet og nægte adgang til særligt sensitive handlinger, hvis 2FA ikke er bekræftet.

Alle OIDC-hændelser bør logges med tilstrækkelige oplysninger til revisionsspor: tidsstempel, bruger-sub, klientapp-ID, IP-adresse, geolokation (om muligt), og om login lykkedes eller mislykkedes. Disse logs er påkrævet af NIS2-direktivets artikel 21, som kræver logning og monitering af sikkerhedshændelser. Gem logfiler i minimum 12 måneder, og sørg for, at de er skrivebeskyttede og ikke kan slettes af applikationskoden.

Komplet projektfil: auth.js med alle funktioner

Her er den komplette auth.js-fil med alle funktioner samlet, klar til at kopiere direkte ind i dit projekt. Filen inkluderer login, callback, logout, userinfo-hentning og fejlhåndtering.

// auth.js - Komplet OIDC-klientimplementation
import * as client from 'openid-client';

let oidcConfig;

export async function getOidcConfig() {
  if (oidcConfig) return oidcConfig;
  oidcConfig = await client.discovery(
    new URL(process.env.ISSUER_URL),
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET
  );
  return oidcConfig;
}

export async function login(req, res) {
  const config = await getOidcConfig();

  const codeVerifier = client.randomPKCECodeVerifier();
  const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
  const state = client.randomState();
  const nonce = client.randomNonce();

  // Gem i session (server-side)
  Object.assign(req.session, { codeVerifier, state, nonce });

  const authUrl = client.buildAuthorizationUrl(config, {
    redirect_uri: process.env.REDIRECT_URI,
    scope: 'openid profile email offline_access',
    response_type: 'code',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state,
    nonce,
  });

  res.redirect(authUrl.href);
}

export async function callback(req, res) {
  const config = await getOidcConfig();

  try {
    const currentUrl = new URL(
      req.url,
      `${req.protocol}://${req.hostname}:${process.env.PORT}`
    );

    const tokens = await client.authorizationCodeGrant(config, currentUrl, {
      pkceCodeVerifier: req.session.codeVerifier,
      expectedState: req.session.state,
      expectedNonce: req.session.nonce,
    });

    const claims = tokens.claims();

    req.session.user = {
      sub: claims.sub,
      email: claims.email,
      name: claims.name,
      auth_time: claims.auth_time,
      accessToken: tokens.access_token,
      refreshToken: tokens.refresh_token,
      idToken: tokens.id_token,
    };

    // Ryd engangsbrug PKCE-parametre
    delete req.session.codeVerifier;
    delete req.session.state;
    delete req.session.nonce;

    const returnTo = req.session.returnTo || '/profile';
    delete req.session.returnTo;

    res.redirect(returnTo);

  } catch (err) {
    console.error('[OIDC] Callback-fejl:', err.message);
    res.status(400).render('error', { message: 'Login mislykkedes. Prøv igen.' });
  }
}

export async function logout(req, res) {
  const config = await getOidcConfig();
  const idToken = req.session.user?.idToken;
  const baseUrl = `${req.protocol}://${req.hostname}:${process.env.PORT}`;

  req.session.destroy(() => {
    try {
      const logoutUrl = client.buildEndSessionUrl(config, {
        post_logout_redirect_uri: baseUrl,
        id_token_hint: idToken,
      });
      res.redirect(logoutUrl.href);
    } catch {
      res.redirect('/');
    }
  });
}

export async function refreshAccessToken(req) {
  const config = await getOidcConfig();
  const tokens = await client.refreshTokenGrant(
    config,
    req.session.user.refreshToken
  );
  req.session.user.accessToken = tokens.access_token;
  if (tokens.refresh_token) {
    req.session.user.refreshToken = tokens.refresh_token; // rotation
  }
  return tokens.access_token;
}

Den komplette auth.js-fil inkluderer offline_access-scopet i login-anmodningen for at få et refresh token. Funktionen refreshAccessToken() bruger det gemte refresh token til at hente et nyt access token, når det aktuelle udløber. Husk at opdatere refresh tokenet i sessionen, hvis Identity Provideren bruger token rotation og returnerer et nyt refresh token i svaret.

Relateret dækning

Disse artikler fra shattered.io dækker emner, der supplerer OAuth 2.0 og OIDC-implementeringen.

Ofte stillede spørgsmål

Hvad er forskellen på OAuth 2.0 og OpenID Connect i praksis?

OAuth 2.0 giver applikationen tilladelse til at handle på brugerens vegne over for en API, men siger intet om brugerens identitet. OpenID Connect tilføjer et ID-token, der bekræfter, hvem brugeren er. I praksis bruger du OIDC til brugerlogin og OAuth 2.0 til API-adgang. De to fungerer typisk sammen: OIDC-flowet udsteder både et ID-token (hvem brugeren er) og et access token (hvad applikationen må).

Skal jeg bruge PKCE, hvis min applikation har en client secret?

Ja. PKCE tilføjer et ekstra sikkerhedslag, selv for server-side applikationer med klienthemmeligheder. OAuth 2.1-udkastet kræver PKCE for alle klienttyper. Client secrets beskytter token-endepunktet, mens PKCE beskytter selve autoriseringskoden under transit. Begge mekanismer løser forskellige angrebsvektorer og komplementerer hinanden.

Kan jeg implementere OAuth 2.0 uden et bibliotek som openid-client?

Teknisk set ja, men det frarådes kraftigt. Korrekt JWT-validering kræver implementering af RS256/ES256-signaturverifikation, nøglerotation via JWKS-endepunktet, claims-validering og mere. Et enkelt fejltrin, f.eks. at acceptere “none”-algoritmen for JWT-signaturer, kan give komplet authentication bypass. Brug altid en OpenID Certified™ implementation til produktionskode.

Hvad sker der, hvis min Identity Provider er nede under et login-forsøg?

Brugere, der allerede er logget ind og har en aktiv session, påvirkes ikke. Nye login-forsøg fejler med en connection timeout. Implementér en brugervenlig fejlside, der forklarer situationen, og overvej at monitorere Identity Providerens statussider. Okta og Auth0 tilbyder begge 99,99 % oppetid-garantier med status-sider på henholdsvis status.okta.com og status.auth0.com.

Hvad er forskellen på openid-client og passport-openidconnect?

openid-client er den officielle OpenID Certified™ OIDC-klientimplementation for Node.js, vedligeholdt af panva og brugt af store projekter som Next-Auth. Den håndterer discovery, token-validering, PKCE og userinfo automatisk. passport-openidconnect er en Passport.js-strategi, der er ældre og ikke har samme grad af automatisk certifisering. For nye projekter i 2026 anbefales openid-client.

Hvilken OIDC Identity Provider skal jeg vælge til et dansk startup?

Okta og Auth0 (som er ejet af Okta) tilbyder begge gratis udviklertier med op til 7.500 månedlige aktive brugere. Keycloak er et open source-alternativ, du selv kan hoste, ideelt for virksomheder med strenge datalokalitetskrav i henhold til GDPR. Microsoft Entra ID (tidligere Azure AD) er oplagt, hvis din organisation allerede bruger Microsoft 365. Vælg en udbyder, der har SOC 2 Type II-certificering og tilbyder dataopbevaring inden for EU, hvis du håndterer personoplysninger for europæiske brugere.

Hvad er den rigtige tilgang til brugerlogin i en Next.js-applikation?

I Next.js-applikationer anbefales biblioteket Auth.js (tidligere NextAuth.js), som bruger openid-client internt og understøtter 50+ Identity Providere med minimal konfiguration. Alternativt kan du bruge Oktas @okta/nextjs-sdk eller Auth0s @auth0/nextjs-auth0. Disse biblioteker implementerer BFF-mønsteret (Backend For Frontend) automatisk og gemmer tokens server-side i cookies eller sessions, aldrig i localStorage.