{"id":106,"date":"2026-06-17T16:34:22","date_gmt":"2026-06-17T16:34:22","guid":{"rendered":"https:\/\/shattered.io\/dk\/2026\/06\/17\/oauth2-openid-connect-nodejs\/"},"modified":"2026-06-17T16:35:44","modified_gmt":"2026-06-17T16:35:44","slug":"oauth2-openid-connect-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/dk\/oauth2-openid-connect-nodejs\/","title":{"rendered":"OAuth 2.0 og OpenID Connect i Node.js: 12 trin p\u00e5 30 min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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\u00e6stmest udbredte \u00e5rsag til autentificeringsbrud if\u00f8lge OWASPs Top 10 2025-rapport. Denne guide leder dig igennem 12 pr\u00e6cise trin til et fuldst\u00e6ndigt og sikkert OIDC-login med Node.js, Express og <strong>openid-client v6.8.4<\/strong>, den eneste OpenID Certified\u2122 OAuth 2 \/ OpenID Connect-klient for JavaScript-k\u00f8rselsmilj\u00f8er. Du bygger en komplet applikation med PKCE, token-validering og beskyttede ruter p\u00e5 under 30 minutter.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hvad-er-oauth-2-0-og-openid-connect\">Hvad er OAuth 2.0 og OpenID Connect?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 er en autorisationsprotokol, defineret i <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" rel=\"noopener noreferrer\" target=\"_blank\">RFC 6749<\/a> fra 2012. Den giver tredjepartsapplikationer adgang til beskyttede ressourcer p\u00e5 vegne af en bruger uden at kende brugerens kodeord. En OAuth 2.0-server udsteder kortvarige <em>access tokens<\/em>, som applikationen bruger til at kalde API&#8217;er. OAuth 2.0 handler udelukkende om delegeret autorisation, ikke om hvem brugeren er.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OpenID Connect (OIDC) l\u00f8ser det manglende led: identitet. OIDC er et identitetslag oven p\u00e5 OAuth 2.0, specificeret af <a href=\"https:\/\/openid.net\/connect\/\" rel=\"noopener noreferrer\" target=\"_blank\">OpenID Foundation<\/a>. Det tilf\u00f8jer et <em>ID-token<\/em>, 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 &#8220;Log ind med Google&#8221; og &#8220;Log ind med Microsoft&#8221; p\u00e5 millioner af hjemmesider.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I praksis bruger du altid OIDC, n\u00e5r du vil autentificere brugere. Du bruger rent OAuth 2.0, n\u00e5r du kun har brug for adgang til en API, f.eks. at l\u00e6se en brugers Google Calendar-begivenheder, uden at logge dem ind p\u00e5 dit eget system. Forskellen er afg\u00f8rende for at v\u00e6lge den rigtige flow og de rigtige scopes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e6cis det, du implementerer her.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"oauth-2-0-mod-openid-connect-kerneforskellene\">OAuth 2.0 mod OpenID Connect: Kerneforskellene<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mange udviklere bruger OAuth 2.0 og OIDC i fl\u00e6ng, men de l\u00f8ser fundamentalt forskellige problemer. Tabellen nedenfor viser de tre mest brugte autentificerings- og autorisationsprotokoller side om side.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Protokol<\/th><th>Form\u00e5l<\/th><th>Token-type<\/th><th>Prim\u00e6r use case<\/th><th>Udstedt af<\/th><\/tr><\/thead><tbody><tr><td>OAuth 2.0<\/td><td>Delegeret autorisation<\/td><td>Access Token (opaque eller JWT)<\/td><td>API-adgang p\u00e5 vegne af bruger<\/td><td>Authorization Server<\/td><\/tr><tr><td>OpenID Connect<\/td><td>Autentificering + autorisation<\/td><td>ID-token (JWT) + Access Token<\/td><td>Brugerlogin, SSO<\/td><td>Identity Provider (IdP)<\/td><\/tr><tr><td>SAML 2.0<\/td><td>Enterprise SSO<\/td><td>XML-assertion<\/td><td>Virksomhedslogin, legacy-systemer<\/td><td>Identity Provider<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 Access Tokens indeholder typisk kun en bruger-ID og udl\u00f8bstid. ID-tokens fra OIDC indeholder derimod verifikation af autentificeringstidspunkt (<code>auth_time<\/code>), nonce til replay-beskyttelse og brugeroplysninger (claims) som <code>email<\/code>, <code>name<\/code> og <code>sub<\/code> (subject identifier). Brug altid ID-tokenet til at fastsl\u00e5 brugerens identitet, og brug access tokenet til at kalde beskyttede API-endepunkter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En kritisk fejl, mange udviklere beg\u00e5r, 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\u00f8bstiden og audience-claimet (<code>aud<\/code>) i ID-tokenet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"authorization-code-flow-med-pkce-standardflowet-i-2026\">Authorization Code Flow med PKCE: Standardflowet i 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Authorization Code Flow med PKCE (Proof Key for Code Exchange, <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" rel=\"noopener noreferrer\" target=\"_blank\">RFC 7636<\/a>) er det eneste anbefalede flow for webapplikationer og mobile apps i 2026. Flowet forhindrer <em>authorization code interception attacks<\/em>, en angrebstype, hvor en ondsindet app p\u00e5 enheden opsnapper autoriseringskoden, f\u00f8r den n\u00e5r din applikation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sekvensen ser s\u00e5dan ud i praksis: Din applikation genererer en tilf\u00e6ldig <code>code_verifier<\/code> (minimum 43 tegn). Den beregner derefter en <code>code_challenge<\/code> ved at SHA-256-hashe verifikatoren og base64url-encode resultatet. Autoriseringskoden, der sendes til identity provideren, indeholder kun udfordringen, ikke verifikatoren. N\u00e5r autoriseringskoden returneres til din callback-URL, sender du verifikatoren i token-anmodningen. Identity provideren matcher udfordringen med den hashede verifikator og bekr\u00e6fter dermed, at token-anmodningen kommer fra den applikation, der startede flowen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE kombineres med <code>state<\/code>-parameteren til CSRF-beskyttelse og <code>nonce<\/code>-parameteren til replay-angrebsbeskyttelse. Alle tre parametre er obligatoriske i en sikker implementation. <code>openid-client v6<\/code> h\u00e5ndterer automatisk generering og validering af alle tre, hvis du bruger bibliotekets hj\u00e6lpefunktioner.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"forudsaetninger\">Foruds\u00e6tninger<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Inden du begynder, skal f\u00f8lgende software og konti v\u00e6re klar.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Krav<\/th><th>Version \/ detaljer<\/th><th>Form\u00e5l<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>v22.0+ (LTS anbefales)<\/td><td>JavaScript-k\u00f8rselsmilj\u00f8<\/td><\/tr><tr><td>npm<\/td><td>v10.0+<\/td><td>Pakkeh\u00e5ndtering<\/td><\/tr><tr><td>openid-client<\/td><td>v6.8.4<\/td><td>OIDC-klient (OpenID Certified\u2122)<\/td><\/tr><tr><td>express<\/td><td>v4.21+<\/td><td>HTTP-framework<\/td><\/tr><tr><td>express-session<\/td><td>v1.19.0<\/td><td>Server-side session<\/td><\/tr><tr><td>dotenv<\/td><td>v16.x<\/td><td>Milj\u00f8variabelh\u00e5ndtering<\/td><\/tr><tr><td>Identity Provider-konto<\/td><td>Okta (gratis udviklerkonto) eller Auth0<\/td><td>OIDC-server<\/td><\/tr><tr><td>Grundl\u00e6ggende Node.js-kendskab<\/td><td>Express, async\/await<\/td><td>Foruds\u00e6tning<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Kontroll\u00e9r din Node.js-version med <code>node --version<\/code>. K\u00f8rselsmilj\u00f8er \u00e6ldre end Node.js v18 underst\u00f8tter ikke Web Crypto API, som <code>openid-client v6<\/code> bruger internt til kryptografiske operationer. Node.js v22 LTS er st\u00e6rkt anbefalet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>openid-client v6<\/strong> er et rent ES-modul (ESM). Du kan enten bruge <code>type: \"module\"<\/code> i din <code>package.json<\/code> eller importere det dynamisk med <code>await import()<\/code> i CommonJS-filer. Denne guide bruger ES-moduler fra start.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-1-opret-projektet-og-installer-afhaengigheder\">Trin 1: Opret projektet og install\u00e9r afh\u00e6ngigheder<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start med at oprette en ny projektmappe og initialisere et Node.js-projekt. Brug <code>--yes<\/code>-flaget til at springe det interaktive setup over.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">mkdir oidc-demo && cd oidc-demo\nnpm init --yes\nnpm install express openid-client express-session dotenv<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tilf\u00f8j <code>\"type\": \"module\"<\/code> til din <code>package.json<\/code> for at aktivere ES-moduler. \u00c5bn filen og opdater den:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n  \"name\": \"oidc-demo\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"app.js\",\n  \"scripts\": {\n    \"start\": \"node app.js\",\n    \"dev\": \"node --watch app.js\"\n  },\n  \"dependencies\": {\n    \"dotenv\": \"^16.4.7\",\n    \"express\": \"^4.21.2\",\n    \"express-session\": \"^1.19.0\",\n    \"openid-client\": \"^6.8.4\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Projektstrukturen ser s\u00e5dan ud, n\u00e5r du er f\u00e6rdig:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">oidc-demo\/\n\u251c\u2500\u2500 .env              # Milj\u00f8variabler (aldrig i versionskontrol)\n\u251c\u2500\u2500 .env.example      # Skabelon til andre udviklere\n\u251c\u2500\u2500 .gitignore        # Ekskluderer .env og node_modules\n\u251c\u2500\u2500 app.js            # Hovedapplikation\n\u251c\u2500\u2500 auth.js           # OIDC-klient og hj\u00e6lpefunktioner\n\u251c\u2500\u2500 middleware.js     # Middleware til beskyttede ruter\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Opret en <code>.gitignore<\/code>-fil med det samme for at sikre, at hemmeligheder ikke ender i versionskontrol:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">node_modules\/\n.env\n*.log<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-2-3-konfigurer-identity-provider-og-miljoevariabler\">Trin 2-3: Konfigurer Identity Provider og milj\u00f8variabler<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Denne guide bruger Okta som Identity Provider, men trinene er n\u00e6sten identiske for Auth0, Keycloak og andre OIDC-kompatible udbydere. Opret en gratis Okta Developer-konto p\u00e5 <a href=\"https:\/\/developer.okta.com\" rel=\"noopener noreferrer\" target=\"_blank\">developer.okta.com<\/a> og f\u00f8lg disse trin:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trin 2: Konfigurer applikationen i Okta<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log ind i Okta-administrationspanelet og naviger til <strong>Applications &gt; Applications<\/strong>. Klik p\u00e5 <strong>Create App Integration<\/strong> og v\u00e6lg <strong>OIDC &#8211; OpenID Connect<\/strong> som login-metode og <strong>Web Application<\/strong> som applikationstype. Udfyld f\u00f8lgende felter:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Appintegrationsnavn: <code>OIDC Demo Node.js<\/code>. Sign-in redirect URIs: <code>http:\/\/localhost:3000\/callback<\/code>. Sign-out redirect URIs: <code>http:\/\/localhost:3000<\/code>. Klik p\u00e5 <strong>Save<\/strong>. Okta viser nu dit <strong>Client ID<\/strong> og <strong>Client Secret<\/strong>. Gem disse to v\u00e6rdier straks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Find din Okta-dom\u00e6ne-URL \u00f8verst til h\u00f8jre i administrationspanelet under dit kontonavn (formatet er <code>https:\/\/dev-xxxxxxx.okta.com<\/code>). Tilf\u00f8j <code>\/oauth2\/default<\/code> for at f\u00e5 den fulde issuer-URL: <code>https:\/\/dev-xxxxxxx.okta.com\/oauth2\/default<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Under <strong>Advanced Settings<\/strong> i din applikationskonfiguration, s\u00f8rg for:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Token Endpoint Authentication Method<\/strong>: s\u00e6t til <code>client_secret_basic<\/code>. Dette er den sikreste metode til server-side applikationer, da klienthemmeligheden sendes i HTTP Authorization-headeren, base64-kodet, og aldrig eksponeres i URL&#8217;en.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trin 3: Opret .env-filen<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Opret en <code>.env<\/code>-fil i projektets rodmappe med de v\u00e6rdier, du netop hentede fra Okta. Erstat eksemplev\u00e6rdierne med dine faktiske Okta-legitimationsoplysninger.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\"># .env - ALDRIG commit denne fil til versionskontrol\nISSUER_URL=https:\/\/dev-xxxxxxx.okta.com\/oauth2\/default\nCLIENT_ID=0oa1234567890abcdef\nCLIENT_SECRET=din-hemmelige-noegle-her\nREDIRECT_URI=http:\/\/localhost:3000\/callback\nSESSION_SECRET=et-langt-tilfaeldigt-hemmeligt-ord-minimum-32-tegn\nPORT=3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Opret ogs\u00e5 en <code>.env.example<\/code>-fil med tomme v\u00e6rdier, som du kan inkludere i versionskontrol som dokumentation for andre udviklere. Gentag aldrig klienthemmeligheder, API-n\u00f8gler eller session-hemmeligheder i kildekoden. Disse oplysninger giver en angriber fuld adgang til at udgive sig som din applikation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-4-5-express-og-session-middleware\">Trin 4-5: Express og session-middleware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Opret den centrale <code>app.js<\/code>-fil. Session-konfigurationen er kritisk for sikkerheden: du gemmer tempor\u00e6re PKCE-parametre og brugeroplysninger i serveren, ikke i cookies alene.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ app.js\nimport 'dotenv\/config';\nimport express from 'express';\nimport session from 'express-session';\nimport { login, callback, logout, getOidcConfig } from '.\/auth.js';\nimport { requireAuth } from '.\/middleware.js';\n\nconst app = express();\n\n\/\/ Sikkerheds-headers\napp.set('trust proxy', 1); \/\/ P\u00e5kr\u00e6vet bag en reverse proxy i produktion\n\n\/\/ Session-middleware: gemmer PKCE-parametre server-side\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    httpOnly: true,         \/\/ Forhindrer JavaScript-adgang til cookie\n    secure: process.env.NODE_ENV === 'production', \/\/ HTTPS-only i produktion\n    sameSite: 'lax',        \/\/ CSRF-beskyttelse\n    maxAge: 60 * 60 * 1000 \/\/ 1 time i millisekunder\n  }\n}));\n\n\/\/ Initialiser OIDC-klienten ved opstart\nawait getOidcConfig();\n\n\/\/ Ruter\napp.get('\/login', login);\napp.get('\/callback', callback);\napp.get('\/logout', logout);\n\n\/\/ Beskyttet rute: requireAuth middleware verificerer session\napp.get('\/profile', requireAuth, (req, res) => {\n  const user = req.session.user;\n  res.json({\n    sub: user.sub,\n    email: user.email,\n    name: user.name,\n    loggedInAt: new Date(user.auth_time * 1000).toISOString()\n  });\n});\n\n\/\/ Offentlig rute\napp.get('\/', (req, res) => {\n  const isLoggedIn = !!req.session.user;\n  res.send(`\n    <h1>OIDC Demo<\/h1>\n    ${isLoggedIn\n      ? `<p>Logget ind som ${req.session.user.email}<\/p><a href=\"\/profile\">Se profil<\/a> | <a href=\"\/logout\">Log ud<\/a>`\n      : `<a href=\"\/login\">Log ind med Okta<\/a>`\n    }\n  `);\n});\n\nconst port = process.env.PORT || 3000;\napp.listen(port, () => {\n  console.log(`Server k\u00f8rer p\u00e5 http:\/\/localhost:${port}`);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cookie-indstillingerne <code>httpOnly: true<\/code> og <code>sameSite: 'lax'<\/code> er ikke valgfrie. <code>httpOnly<\/code> forhindrer JavaScript p\u00e5 klientsiden i at l\u00e6se session-cookien, hvilket eliminerer risikoen for, at XSS-angreb stj\u00e6ler sessioner. <code>sameSite: 'lax'<\/code> tillader cookies at f\u00f8lge med ved normale navigationer (klik p\u00e5 link), men blokerer dem i cross-site POST-anmodninger, der bruges i CSRF-angreb.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>secure: true<\/code> i produktion sikrer, at session-cookien kun sendes over HTTPS-forbindelser. I udviklingsmilj\u00f8et s\u00e6ttes dette til <code>false<\/code> via <code>NODE_ENV<\/code>-variablen, da localhost typisk k\u00f8rer over HTTP.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-6-7-oidc-discovery-og-login-flow-med-pkce\">Trin 6-7: OIDC Discovery og login-flow med PKCE<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Opret <code>auth.js<\/code>-filen med den fulde OIDC-implementation. Brug <code>client.discovery()<\/code> til automatisk at hente alle n\u00f8dvendige endepunkts-URL&#8217;er fra Identity Providerens <code>.well-known\/openid-configuration<\/code>-dokument.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ auth.js\nimport * as client from 'openid-client';\n\nlet oidcConfig;\n\n\/\/ Trin 6: OIDC Discovery - hent konfiguration fra Identity Provider\nexport async function getOidcConfig() {\n  if (oidcConfig) return oidcConfig;\n\n  oidcConfig = await client.discovery(\n    new URL(process.env.ISSUER_URL),\n    process.env.CLIENT_ID,\n    process.env.CLIENT_SECRET\n  );\n\n  console.log('OIDC Discovery gennemf\u00f8rt:', process.env.ISSUER_URL);\n  return oidcConfig;\n}\n\n\/\/ Trin 7: Start login-flow med PKCE\nexport async function login(req, res) {\n  const config = await getOidcConfig();\n\n  \/\/ Generer PKCE-parametre og sikkerhedsparametre\n  const codeVerifier = client.randomPKCECodeVerifier();\n  const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);\n  const state = client.randomState();\n  const nonce = client.randomNonce();\n\n  \/\/ Gem parametre i server-side session (ikke i cookie)\n  req.session.codeVerifier = codeVerifier;\n  req.session.state = state;\n  req.session.nonce = nonce;\n\n  \/\/ Byg autoriserings-URL\n  const authorizationUrl = client.buildAuthorizationUrl(config, {\n    redirect_uri: process.env.REDIRECT_URI,\n    scope: 'openid profile email',\n    response_type: 'code',\n    code_challenge: codeChallenge,\n    code_challenge_method: 'S256',\n    state,\n    nonce,\n  });\n\n  \/\/ Omdiriger brugeren til Identity Provider\n  res.redirect(authorizationUrl.href);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>client.randomPKCECodeVerifier()<\/code> genererer en kryptografisk st\u00e6rk tilf\u00e6ldig streng p\u00e5 43-128 tegn ved brug af Web Crypto API. <code>client.calculatePKCECodeChallenge()<\/code> hasher verifikatoren med SHA-256 og base64url-encoder resultatet. <code>code_challenge_method: 'S256'<\/code> specificerer, at SHA-256-metoden bruges, i mods\u00e6tning til den svagere og for\u00e6ldede <code>plain<\/code>-metode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e6se cookies.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-8-9-callback-handler-og-token-validering\">Trin 8-9: Callback-handler og token-validering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Callback-handleren modtager autoriseringskoden fra Identity Provideren og udveksler den til tokens. <code>openid-client<\/code> validerer automatisk ID-tokenets signatur, udl\u00f8bstid, issuer, audience, nonce og state.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ Forts\u00e6ttelse af auth.js\n\n\/\/ Trin 8: Callback-handler - modtag autoriseringskode og udveksl til tokens\nexport async function callback(req, res) {\n  const config = await getOidcConfig();\n\n  try {\n    \/\/ Rekonstruer den aktuelle URL fra anmodningen\n    const currentUrl = new URL(\n      req.url,\n      `${req.protocol}:\/\/${req.hostname}:${process.env.PORT}`\n    );\n\n    \/\/ Udveksl autoriseringskode til tokens (validerer automatisk state og PKCE)\n    const tokens = await client.authorizationCodeGrant(config, currentUrl, {\n      pkceCodeVerifier: req.session.codeVerifier,   \/\/ PKCE-validering\n      expectedState: req.session.state,              \/\/ CSRF-beskyttelse\n      expectedNonce: req.session.nonce,              \/\/ Replay-beskyttelse\n    });\n\n    \/\/ Trin 9: Hent og gem brugeroplysninger fra ID-token\n    const claims = tokens.claims();\n\n    \/\/ Gem brugeroplysninger i session\n    req.session.user = {\n      sub: claims.sub,           \/\/ Unik bruger-ID hos Identity Provider\n      email: claims.email,\n      name: claims.name,\n      auth_time: claims.auth_time,\n      accessToken: tokens.access_token,\n    };\n\n    \/\/ Ryd PKCE-parametre fra session (de er kun n\u00f8dvendige \u00e9n gang)\n    delete req.session.codeVerifier;\n    delete req.session.state;\n    delete req.session.nonce;\n\n    \/\/ Omdiriger til beskyttet side\n    res.redirect('\/profile');\n\n  } catch (err) {\n    console.error('OIDC callback-fejl:', err.message);\n    res.status(400).send(`Autentificeringsfejl: ${err.message}`);\n  }\n}\n\n\/\/ Trin 11: Logout\nexport async function logout(req, res) {\n  const config = await getOidcConfig();\n  const idToken = req.session.user?.idToken;\n\n  \/\/ \u00d8del\u00e6g den lokale session\n  req.session.destroy((err) => {\n    if (err) console.error('Session-destroy fejl:', err);\n\n    \/\/ Byg end-session URL hos Identity Provider\n    const logoutUrl = client.buildEndSessionUrl(config, {\n      post_logout_redirect_uri: `${req.protocol}:\/\/${req.hostname}:${process.env.PORT}`,\n      id_token_hint: idToken,\n    });\n\n    res.redirect(logoutUrl?.href || '\/');\n  });\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>client.authorizationCodeGrant()<\/code> udf\u00f8rer automatisk f\u00f8lgende verifikationstrin: den henter tokens fra token-endepunktet med <code>client_secret_basic<\/code>-autentificering, verificerer ID-tokenets JWT-signatur mod Identity Providerens offentlige n\u00f8gler, kontrollerer <code>iss<\/code>-claimet mod den forventede issuer, kontrollerer <code>aud<\/code>-claimet mod dit Client ID, kontrollerer <code>exp<\/code>-claimet og sikrer, at tokenet ikke er udl\u00f8bet, verificerer <code>nonce<\/code>-claimet mod den gemte nonce for at forhindre replay-angreb, og verificerer <code>state<\/code>-parameteren mod den gemte state for at forhindre CSRF.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Disse verifikationstrin sker p\u00e5 \u00e9n kodelinje. En manuel implementation af blot \u00e9t af disse trin tager op til en times arbejde og er fyldt med potentielle fejl. Det er pr\u00e6cis grunden til, at en OpenID Certified\u2122-klientimplementation er afg\u00f8rende for produktionskode.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-10-middleware-til-beskyttede-ruter\">Trin 10: Middleware til beskyttede ruter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Opret <code>middleware.js<\/code> med et enkelt middleware, der verificerer, om brugeren er autentificeret, og omdirigerer til login-siden, hvis ikke.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ middleware.js\n\n\/\/ Middleware til beskyttelse af ruter\nexport function requireAuth(req, res, next) {\n  if (!req.session.user) {\n    \/\/ Gem den \u00f8nskede URL i session, s\u00e5 brugeren returneres efter login\n    req.session.returnTo = req.originalUrl;\n    return res.redirect('\/login');\n  }\n  next();\n}\n\n\/\/ Middleware til at injicere brugeroplysninger i alle skabeloner\nexport function injectUser(req, res, next) {\n  res.locals.user = req.session.user || null;\n  next();\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Brug <code>requireAuth<\/code> som middleware p\u00e5 alle ruter, der kr\u00e6ver login. I <code>app.js<\/code> s\u00e6ttes det direkte i route-definitionen: <code>app.get('\/dashboard', requireAuth, dashboardController)<\/code>. Middlewaret er genbrugeligt p\u00e5 tv\u00e6rs af alle beskyttede endpoints og kr\u00e6ver ikke duplikering af autentificeringskontrollogik.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 (<code>connect-redis<\/code>-pakken) eller PostgreSQL. In-memory sessions mistes ved genstart af applikationen, og de skalerer ikke til flere Node.js-instanser.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-11-12-test-applikationen-og-se-output\">Trin 11-12: Test applikationen og se output<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start applikationen og test hele login-flowet:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">node app.js\n# Output:\n# OIDC Discovery gennemf\u00f8rt: https:\/\/dev-xxxxxxx.okta.com\/oauth2\/default\n# Server k\u00f8rer p\u00e5 http:\/\/localhost:3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c5bn <code>http:\/\/localhost:3000<\/code> i din browser. Du ser hjemmesiden med linket &#8220;Log ind med Okta&#8221;. Klik p\u00e5 linket og gennemf\u00f8r Okta-login-flowet. Efter succesfuld autentificering omdirigeres du til <code>\/profile<\/code>, hvor du ser en JSON-respons som denne:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n  \"sub\": \"00u1a2b3c4d5e6f7g8h9\",\n  \"email\": \"bruger@eksempel.dk\",\n  \"name\": \"S\u00f8ren Hansen\",\n  \"loggedInAt\": \"2026-06-17T10:30:45.000Z\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kontroll\u00e9r netv\u00e6rkstrafikken i browserens udviklingsv\u00e6rkt\u00f8jer under login-flowet. Du kan se tre HTTP-anmodninger: 1) <code>GET \/login<\/code>, som omdirigerer til Oktas autorisations-URL med <code>code_challenge<\/code>-parameteren, 2) <code>GET \/callback?code=...&state=...<\/code>, som modtager autoriseringskoden fra Okta, og 3) en POST-anmodning fra din server til Oktas token-endepunkt, som kun er synlig i serverloggen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Verificer PKCE-sikkerheden ved at fors\u00f8ge at bes\u00f8ge <code>\/callback<\/code> direkte uden en aktiv session med PKCE-parametrene gemt. Applikationen svarer med en 400-fejl: &#8220;OIDC callback-fejl: invalid_grant&#8221; eller lignende, fordi <code>code_verifier<\/code>-parameteren mangler i session.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fem-kritiske-faldgruber-ved-oauth-2-0-og-oidc\">Fem kritiske faldgruber ved OAuth 2.0 og OIDC<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Disse fejl dukker op i produktionskode igen og igen. Ingen af dem er \u00e5benlyse for en ny udvikler, og alle kan f\u00f8re til alvorlige sikkerhedsbrud.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 1: Udelader state-parameteren<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">State-parameteren beskytter mod CSRF-angreb i OAuth-flowet. Uden den kan en angriber lokke en bruger til at gennemf\u00f8re et login-flow, der binder brugerens konto til angriberens session (login CSRF). openid-client v6 genererer state automatisk via <code>client.randomState()<\/code>, men det er dit ansvar at gemme og verificere den i session. Aldrig spring denne validering over, selv i prototyper.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 2: Bruger Implicit Flow i 2026<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Implicit Flow returnerer tokens direkte i URL-fragmentet (<code>#access_token=...<\/code>). Tokens i URL&#8217;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&#8217;er.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 3: Gemmer access tokens i localStorage<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">localStorage er tilg\u00e6ngeligt via JavaScript, hvilket g\u00f8r alle gemte tokens s\u00e5rbare over for XSS-angreb. Et XSS-angreb p\u00e5 \u00e9t sted p\u00e5 din side stj\u00e6ler alle tokens fra localStorage. Gem tokens server-side i sessions, og brug httpOnly-cookies til session-ID&#8217;et. Browsersidede SPA&#8217;er uden backend b\u00f8r bruge token-rotation og BFF-m\u00f8nsteret (Backend For Frontend).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 4: Ingen nonce-validering<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Nonce forhindrer replay-angreb, hvor en angriber genbruger et afsnappet ID-token til at starte en ny session. Generer en tilf\u00e6ldig nonce ved hvert login-fors\u00f8g med <code>client.randomNonce()<\/code>, gem den i session og konfigurer <code>expectedNonce<\/code> i <code>authorizationCodeGrant()<\/code>-kaldet. openid-client verificerer automatisk nonce-claimet i ID-tokenet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 5: Validerer ikke redirect_uri pr\u00e6cist<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En \u00e5ben redirect-s\u00e5rbarhed opst\u00e5r, hvis Identity Provideren accepterer redirect URIs, der delvist matcher. S\u00f8rg for, at din Identity Provider kun tillader din pr\u00e6cise redirect URI, inklusiv protokol, port og sti. <code>http:\/\/localhost:3000\/callback<\/code> og <code>http:\/\/localhost:3000\/callback\/<\/code> (med trailing slash) er to forskellige URI&#8217;er og b\u00f8r ikke begge accepteres i produktion. Konfigur\u00e9r altid redirect URIs eksplicit i Identity Providerens administrationspanel.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fejlfinding-10-hyppige-problemer-og-loesninger\">Fejlfinding: 10 hyppige problemer og l\u00f8sninger<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Disse fejlmeddelelser og problemer opst\u00e5r n\u00e6sten uundg\u00e5eligt, f\u00f8rste gang du implementerer OIDC. Her er \u00e5rsagerne og l\u00f8sningerne.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fejlmeddelelse \/ Symptom<\/th><th>Sandsynlig \u00e5rsag<\/th><th>L\u00f8sning<\/th><\/tr><\/thead><tbody><tr><td><code>invalid_grant<\/code><\/td><td>Autoriseringskoden er allerede brugt eller udl\u00f8bet<\/td><td>Autoriseringskoder er engangsbrug og udl\u00f8ber typisk efter 5 minutter. S\u00f8rg for, at callback-handleren kun kalder <code>authorizationCodeGrant()<\/code> \u00e9n gang.<\/td><\/tr><tr><td><code>invalid_client<\/code><\/td><td>Client ID eller Client Secret er forkert<\/td><td>Kontroll\u00e9r .env-filen og sammenlign med Identity Provider-konfigurationen. S\u00f8rg for, at der ikke er mellemrum eller linjeskift i milj\u00f8variablerne.<\/td><\/tr><tr><td><code>redirect_uri_mismatch<\/code><\/td><td>Redirect URI i anmodningen matcher ikke den konfigurerede URI<\/td><td>S\u00f8rg for, at REDIRECT_URI i .env pr\u00e6cis matcher den URI, du har konfigureret i Identity Providerens administrationspanel, tegn for tegn.<\/td><\/tr><tr><td><code>invalid_nonce<\/code><\/td><td>Nonce-claimet i ID-tokenet matcher ikke den gemte nonce<\/td><td>Sessionen er muligvis udl\u00f8bet mellem login og callback. \u00d8g session-timeout eller implementer nonce-persistering i database.<\/td><\/tr><tr><td><code>iss claim mismatch<\/code><\/td><td>Issuer-URL i ID-tokenet matcher ikke konfigurationen<\/td><td>Kontroll\u00e9r ISSUER_URL i .env. Okta-issuer inkluderer typisk <code>\/oauth2\/default<\/code>. Brug pr\u00e6cis den URL, der vises i <code>.well-known\/openid-configuration<\/code>.<\/td><\/tr><tr><td>Bruger sendes i loop til login<\/td><td>Session gemmes ikke korrekt<\/td><td>Tjek at <code>saveUninitialized: false<\/code> og <code>resave: false<\/code> er sat. Verificer at session-hemmeligheden er en lang, tilf\u00e6ldig streng.<\/td><\/tr><tr><td><code>TypeError: Cannot read property 'discovery' of undefined<\/code><\/td><td>openid-client er importeret forkert<\/td><td>S\u00f8rg for at bruge <code>import * as client from 'openid-client'<\/code> og at package.json indeholder <code>\"type\": \"module\"<\/code>.<\/td><\/tr><tr><td>CORS-fejl i browser<\/td><td>SPA fors\u00f8ger at kalde token-endepunkt direkte<\/td><td>Token-udvekslingen skal altid ske server-side. Brug BFF-m\u00f8nsteret. Frontend kalder aldrig token-endepunktet direkte.<\/td><\/tr><tr><td><code>jwks_uri<\/code> kan ikke hentes<\/td><td>Firewallregel blokerer udg\u00e5ende HTTP til Identity Provider<\/td><td>Tillad udg\u00e5ende HTTPS-trafik til Identity Providerens dom\u00e6ne. For Okta: <code>*.okta.com:443<\/code>.<\/td><\/tr><tr><td>Session g\u00e5r tabt ved genstart<\/td><td>In-memory session-store<\/td><td>Install\u00e9r og konfigur\u00e9r <code>connect-redis<\/code> med en Redis-instans. In-memory sessions er kun til udvikling.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Debugging-tip: aktiv\u00e9r detaljeret OIDC-logning ved at s\u00e6tte <code>NODE_DEBUG=openid-client node app.js<\/code> i terminalen. Dette viser alle HTTP-anmodninger og -svar til og fra Identity Provideren, inklusiv den r\u00e5 token-respons og verifikationstrin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerede-teknikker-til-produktionssystemer\">Avancerede teknikker til produktionssystemer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Token-refresh med refresh tokens<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Access tokens udl\u00f8ber typisk efter 1 time. For at undg\u00e5 at brugere logges ud, implementer token-refresh i baggrunden. Anmod om <code>offline_access<\/code>-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\u00f8b ved at kalde token-endepunktet med <code>grant_type=refresh_token<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Multiple Identity Providers<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mange produktionsapplikationer underst\u00f8tter b\u00e5de &#8220;Log ind med Google&#8221; og &#8220;Log ind med Microsoft&#8221;. Kald <code>client.discovery()<\/code> for begge providerUrl&#8217;er ved applikationsstart og cache konfigurationsobjekterne. Brug en <code>provider<\/code>-parameter i login-URL&#8217;en (<code>\/login?provider=google<\/code>) til at v\u00e6lge den korrekte OIDC-konfiguration. Normaliser claims p\u00e5 tv\u00e6rs af providere, da Google bruger <code>email_verified<\/code>, mens Microsoft bruger <code>verified_primary_email<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Enterprise SSO med SAML via OIDC-bro<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e6tter til SAML mod virksomhedens Active Directory. Du beh\u00f8ver ikke implementere SAML direkte i din applikation. Denne arkitektur er den anbefalede tilgang for B2B SaaS-applikationer, der skal underst\u00f8tte enterprise-kunder med eksisterende AD-infrastruktur.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Userinfo-endepunkt for opdaterede claims<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ID-tokenet bages ved login og afspejler ikke \u00e6ndringer i brugerens oplysninger siden da. Kald userinfo-endepunktet for at hente opdaterede claims:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ Hent opdaterede brugeroplysninger fra userinfo-endepunkt\nconst userinfo = await client.fetchUserInfo(\n  config,\n  req.session.user.accessToken,\n  req.session.user.sub\n);\n\/\/ userinfo indeholder de nyeste claims fra Identity Provider<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kald userinfo-endepunktet ved session-fornyelse, ikke ved hvert sideopkald. Cach resultatet i session med en TTL p\u00e5 15 minutter for at undg\u00e5 un\u00f8digt API-kald til Identity Provideren.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sikkerhedshaerdning-til-produktion\">Sikkerhedsh\u00e6rdning til produktion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En funktionerende OIDC-implementation er startpunktet, ikke slutm\u00e5let. Disse trin er n\u00f8dvendige inden produktionsudrulning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>HTTPS er ikke valgfrit.<\/strong> OIDC-flows over HTTP eksponerer tokens i klar tekst. Konfigur\u00e9r din webserver (Nginx, Caddy, Apache) med TLS-certifikat via Let&#8217;s Encrypt. S\u00e6t <code>secure: true<\/code> i session-cookie-konfigurationen og aktiver HSTS-headeren (<code>Strict-Transport-Security: max-age=31536000; includeSubDomains<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Content Security Policy.<\/strong> Tils\u00e6t Helmet.js som middleware for at s\u00e6tte sikkerhedsheaders automatisk:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">npm install helmet<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">import helmet from 'helmet';\n\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [\"'self'\"],\n      scriptSrc: [\"'self'\"],\n      connectSrc: [\"'self'\", process.env.ISSUER_URL],\n      frameSrc: [\"'none'\"],\n    },\n  },\n  hsts: {\n    maxAge: 31536000,\n    includeSubDomains: true,\n  },\n}));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Session-hemmelighed i n\u00f8glestyring.<\/strong> SESSION_SECRET skal v\u00e6re minimum 32 tilf\u00e6ldige bytes genereret med <code>crypto.randomBytes(32).toString('hex')<\/code>. Rot\u00e9r hemmeligheden regelm\u00e6ssigt, og brug en n\u00f8glestyringsservice som HashiCorp Vault, AWS Secrets Manager eller Azure Key Vault i produktion. H\u00e5rd-kodede hemmeligheder er den mest udbredte kilde til legitimationsoplysningsl\u00e6k i Node.js-applikationer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rate limiting p\u00e5 login-endepunktet.<\/strong> Login-flowet starter en omdirigering til Identity Provideren, men et massivt antal anmodninger kan overbelaste din session-store. Kombin\u00e9r din OIDC-implementation med rate limiting:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Se <a href=\"\/da\/rate-limiting-nodejs\/\">Rate Limiting i Node.js: 12 trin, 30 min<\/a> for en komplet implementation med express-rate-limit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Audit-logning.<\/strong> Log alle autentificeringsh\u00e6ndelser: succesfulde logins, mislykkede callbacks og logout-h\u00e6ndelser. Inkluder tidsstempel, bruger-sub, IP-adresse og user-agent. Disse logs er uundv\u00e6rlige ved h\u00e6ndelsesh\u00e5ndtering og kr\u00e6ves af NIS2-direktivet for virksomheder i Danmark, der er underlagt forordningen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"token-livstider-og-session-strategi-hvad-du-skal-vide\">Token-livstider og session-strategi: Hvad du skal vide<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Korrekte token-livstider er afg\u00f8rende for balance mellem sikkerhed og brugeroplevelse. Korte token-livstider begr\u00e6nser skaden, hvis et token kompromitteres, men kr\u00e6ver hyppigere refresh. Lange livstider er mere bekvemme, men \u00f8ger risikoen. Tabellen nedenfor viser typiske anbefalede token-livstider for produktionsapplikationer.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Token-type<\/th><th>Anbefalet livstid<\/th><th>Form\u00e5l<\/th><th>Opbevaring<\/th><\/tr><\/thead><tbody><tr><td>Access Token<\/td><td>15-60 minutter<\/td><td>API-kald til ressourceservere<\/td><td>Hukommelse eller server-session<\/td><\/tr><tr><td>ID Token<\/td><td>15-60 minutter<\/td><td>Brugeridentifikation ved login<\/td><td>Server-session (valideres ved brug)<\/td><\/tr><tr><td>Refresh Token<\/td><td>7-30 dage (med rotation)<\/td><td>Forny access tokens automatisk<\/td><td>Krypteret i database, aldrig i browser<\/td><\/tr><tr><td>Autoriseringskode<\/td><td>1-5 minutter<\/td><td>Engangsbrug: kode-til-token-udveksling<\/td><td>Sendes direkte til callback, gemmes ikke<\/td><\/tr><tr><td>Server-side session<\/td><td>1-8 timer (idle timeout)<\/td><td>Brugerens loginnede tilstand i app<\/td><td>Redis eller lignende session-store<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Implement\u00e9r refresh token rotation for at minimere risikoen ved stj\u00e5lne refresh tokens. N\u00e5r 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\u00e5let et refresh token og fors\u00f8ger at bruge det, efter det er roteret, n\u00e6gtes adgang. Okta og Auth0 underst\u00f8tter begge refresh token rotation som en konfigurationsindstilling i applikationen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Implement\u00e9r altid idle session timeout ud over token-livstider. En bruger, der efterlader sin browser \u00e5ben p\u00e5 et offentligt netv\u00e6rk, b\u00f8r logges ud efter en periode med inaktivitet, uanset om tokens stadig er gyldige. Express-session underst\u00f8tter rolling sessions via <code>rolling: true<\/code>, som forny session-cookien ved hvert request og dermed implementerer idle timeout automatisk.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"oidc-i-danske-virksomheder-compliance-og-gdpr-hensyn\">OIDC i danske virksomheder: Compliance og GDPR-hensyn<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Danske virksomheder, der bruger OIDC-baseret login, skal tage stilling til en r\u00e6kke GDPR-aspekter. Identity Providere som Okta og Auth0 behandler personoplysninger p\u00e5 dine vegne og er dermed databehandlere. Du skal indg\u00e5 en databehandleraftale (DPA) med din Identity Provider, og du skal sikre, at personoplysninger kun behandles i EU\/E\u00d8S-datacentre, med mindre du har et lovligt grundlag for tredjelandsoverf\u00f8rsler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e5 servere i Danmark eller EU, og det giver fuld kontrol over, hvor personoplysninger opbevares.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">NIS2-direktivet, som tr\u00e5dte 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\u00e6rk autentificering. If\u00f8lge Center for Cybersikkerhed (CFCS) var kun 16 % af de virksomheder, der er underlagt NIS2 i Danmark, fuldt compliant ved udgangen af 2025.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For virksomheder, der h\u00e5ndterer s\u00e6rligt f\u00f8lsomme personoplysninger, b\u00f8r du overveje at kombinere OIDC med to-faktor-autentificering. OIDC underst\u00f8tter Authentication Context Class Reference (ACR) claims, som specificerer det autentificeringsniveau, der kr\u00e6ves. Et ACR-claim p\u00e5 <code>urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract<\/code> kr\u00e6ver, at brugeren har gennemf\u00f8rt 2FA, ikke kun kodeordslogin. Din Node.js-applikation kan verificere dette claim i ID-tokenet og n\u00e6gte adgang til s\u00e6rligt sensitive handlinger, hvis 2FA ikke er bekr\u00e6ftet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Alle OIDC-h\u00e6ndelser b\u00f8r logges med tilstr\u00e6kkelige oplysninger til revisionsspor: tidsstempel, bruger-sub, klientapp-ID, IP-adresse, geolokation (om muligt), og om login lykkedes eller mislykkedes. Disse logs er p\u00e5kr\u00e6vet af NIS2-direktivets artikel 21, som kr\u00e6ver logning og monitering af sikkerhedsh\u00e6ndelser. Gem logfiler i minimum 12 m\u00e5neder, og s\u00f8rg for, at de er skrivebeskyttede og ikke kan slettes af applikationskoden.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"komplet-projektfil-auth-js-med-alle-funktioner\">Komplet projektfil: auth.js med alle funktioner<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Her er den komplette <code>auth.js<\/code>-fil med alle funktioner samlet, klar til at kopiere direkte ind i dit projekt. Filen inkluderer login, callback, logout, userinfo-hentning og fejlh\u00e5ndtering.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">\/\/ auth.js - Komplet OIDC-klientimplementation\nimport * as client from 'openid-client';\n\nlet oidcConfig;\n\nexport async function getOidcConfig() {\n  if (oidcConfig) return oidcConfig;\n  oidcConfig = await client.discovery(\n    new URL(process.env.ISSUER_URL),\n    process.env.CLIENT_ID,\n    process.env.CLIENT_SECRET\n  );\n  return oidcConfig;\n}\n\nexport async function login(req, res) {\n  const config = await getOidcConfig();\n\n  const codeVerifier = client.randomPKCECodeVerifier();\n  const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);\n  const state = client.randomState();\n  const nonce = client.randomNonce();\n\n  \/\/ Gem i session (server-side)\n  Object.assign(req.session, { codeVerifier, state, nonce });\n\n  const authUrl = client.buildAuthorizationUrl(config, {\n    redirect_uri: process.env.REDIRECT_URI,\n    scope: 'openid profile email offline_access',\n    response_type: 'code',\n    code_challenge: codeChallenge,\n    code_challenge_method: 'S256',\n    state,\n    nonce,\n  });\n\n  res.redirect(authUrl.href);\n}\n\nexport async function callback(req, res) {\n  const config = await getOidcConfig();\n\n  try {\n    const currentUrl = new URL(\n      req.url,\n      `${req.protocol}:\/\/${req.hostname}:${process.env.PORT}`\n    );\n\n    const tokens = await client.authorizationCodeGrant(config, currentUrl, {\n      pkceCodeVerifier: req.session.codeVerifier,\n      expectedState: req.session.state,\n      expectedNonce: req.session.nonce,\n    });\n\n    const claims = tokens.claims();\n\n    req.session.user = {\n      sub: claims.sub,\n      email: claims.email,\n      name: claims.name,\n      auth_time: claims.auth_time,\n      accessToken: tokens.access_token,\n      refreshToken: tokens.refresh_token,\n      idToken: tokens.id_token,\n    };\n\n    \/\/ Ryd engangsbrug PKCE-parametre\n    delete req.session.codeVerifier;\n    delete req.session.state;\n    delete req.session.nonce;\n\n    const returnTo = req.session.returnTo || '\/profile';\n    delete req.session.returnTo;\n\n    res.redirect(returnTo);\n\n  } catch (err) {\n    console.error('[OIDC] Callback-fejl:', err.message);\n    res.status(400).render('error', { message: 'Login mislykkedes. Pr\u00f8v igen.' });\n  }\n}\n\nexport async function logout(req, res) {\n  const config = await getOidcConfig();\n  const idToken = req.session.user?.idToken;\n  const baseUrl = `${req.protocol}:\/\/${req.hostname}:${process.env.PORT}`;\n\n  req.session.destroy(() => {\n    try {\n      const logoutUrl = client.buildEndSessionUrl(config, {\n        post_logout_redirect_uri: baseUrl,\n        id_token_hint: idToken,\n      });\n      res.redirect(logoutUrl.href);\n    } catch {\n      res.redirect('\/');\n    }\n  });\n}\n\nexport async function refreshAccessToken(req) {\n  const config = await getOidcConfig();\n  const tokens = await client.refreshTokenGrant(\n    config,\n    req.session.user.refreshToken\n  );\n  req.session.user.accessToken = tokens.access_token;\n  if (tokens.refresh_token) {\n    req.session.user.refreshToken = tokens.refresh_token; \/\/ rotation\n  }\n  return tokens.access_token;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Den komplette <code>auth.js<\/code>-fil inkluderer <code>offline_access<\/code>-scopet i login-anmodningen for at f\u00e5 et refresh token. Funktionen <code>refreshAccessToken()<\/code> bruger det gemte refresh token til at hente et nyt access token, n\u00e5r det aktuelle udl\u00f8ber. Husk at opdatere refresh tokenet i sessionen, hvis Identity Provideren bruger token rotation og returnerer et nyt refresh token i svaret.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"relateret-daekning\">Relateret d\u00e6kning<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Disse artikler fra shattered.io d\u00e6kker emner, der supplerer OAuth 2.0 og OIDC-implementeringen.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/da\/jwt-authentication-nodejs\/\">JWT-autentificering i Node.js: 10 trin<\/a> &#8211; Forst\u00e5 JWT-strukturen, der bruges i ID-tokens og access tokens<\/li>\n<li><a href=\"\/da\/nodejs-session-management\/\">Node.js Session Management: 11 trin, 30 min<\/a> &#8211; Dybdeg\u00e5ende guide til sikker session-h\u00e5ndtering i Express<\/li>\n<li><a href=\"\/da\/two-factor-authentication-nodejs\/\">To-faktor-autentificering i Node.js: 11 trin<\/a> &#8211; Tilf\u00f8j TOTP som ekstra sikkerhedslag til dit OIDC-login<\/li>\n<li><a href=\"\/da\/csrf-protection-nodejs\/\">CSRF-beskyttelse i Node.js: 12 trin<\/a> &#8211; Forst\u00e5 CSRF-angreb og state-parameterens rolle i OAuth<\/li>\n<li><a href=\"\/da\/rate-limiting-nodejs\/\">Rate Limiting i Node.js: 12 trin, 30 min<\/a> &#8211; Begr\u00e6ns login-fors\u00f8g og beskyt token-endepunkter<\/li>\n<li><a href=\"\/da\/sikker-session-nodejs\/\">Sikker session i Node.js: 12 trin p\u00e5 30 min<\/a> &#8211; Produktionsklar Redis-baseret session med alle sikkerhedshjeadere<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ofte-stillede-spoergsmaal\">Ofte stillede sp\u00f8rgsm\u00e5l<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad er forskellen p\u00e5 OAuth 2.0 og OpenID Connect i praksis?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 giver applikationen tilladelse til at handle p\u00e5 brugerens vegne over for en API, men siger intet om brugerens identitet. OpenID Connect tilf\u00f8jer et ID-token, der bekr\u00e6fter, 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\u00e5de et ID-token (hvem brugeren er) og et access token (hvad applikationen m\u00e5).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Skal jeg bruge PKCE, hvis min applikation har en client secret?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. PKCE tilf\u00f8jer et ekstra sikkerhedslag, selv for server-side applikationer med klienthemmeligheder. OAuth 2.1-udkastet kr\u00e6ver PKCE for alle klienttyper. Client secrets beskytter token-endepunktet, mens PKCE beskytter selve autoriseringskoden under transit. Begge mekanismer l\u00f8ser forskellige angrebsvektorer og komplementerer hinanden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kan jeg implementere OAuth 2.0 uden et bibliotek som openid-client?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Teknisk set ja, men det frar\u00e5des kraftigt. Korrekt JWT-validering kr\u00e6ver implementering af RS256\/ES256-signaturverifikation, n\u00f8glerotation via JWKS-endepunktet, claims-validering og mere. Et enkelt fejltrin, f.eks. at acceptere &#8220;none&#8221;-algoritmen for JWT-signaturer, kan give komplet authentication bypass. Brug altid en OpenID Certified\u2122 implementation til produktionskode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad sker der, hvis min Identity Provider er nede under et login-fors\u00f8g?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Brugere, der allerede er logget ind og har en aktiv session, p\u00e5virkes ikke. Nye login-fors\u00f8g fejler med en connection timeout. Implement\u00e9r 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\u00e5 henholdsvis status.okta.com og status.auth0.com.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad er forskellen p\u00e5 <code>openid-client<\/code> og <code>passport-openidconnect<\/code>?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>openid-client<\/code> er den officielle OpenID Certified\u2122 OIDC-klientimplementation for Node.js, vedligeholdt af panva og brugt af store projekter som Next-Auth. Den h\u00e5ndterer discovery, token-validering, PKCE og userinfo automatisk. <code>passport-openidconnect<\/code> er en Passport.js-strategi, der er \u00e6ldre og ikke har samme grad af automatisk certifisering. For nye projekter i 2026 anbefales <code>openid-client<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvilken OIDC Identity Provider skal jeg v\u00e6lge til et dansk startup?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Okta og Auth0 (som er ejet af Okta) tilbyder begge gratis udviklertier med op til 7.500 m\u00e5nedlige 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\u00e6lg en udbyder, der har SOC 2 Type II-certificering og tilbyder dataopbevaring inden for EU, hvis du h\u00e5ndterer personoplysninger for europ\u00e6iske brugere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad er den rigtige tilgang til brugerlogin i en Next.js-applikation?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I Next.js-applikationer anbefales biblioteket Auth.js (tidligere NextAuth.js), som bruger <code>openid-client<\/code> internt og underst\u00f8tter 50+ Identity Providere med minimal konfiguration. Alternativt kan du bruge Oktas <code>@okta\/nextjs-sdk<\/code> eller Auth0s <code>@auth0\/nextjs-auth0<\/code>. Disse biblioteker implementerer BFF-m\u00f8nsteret (Backend For Frontend) automatisk og gemmer tokens server-side i cookies eller sessions, aldrig i localStorage.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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\u00e6stmest\u2026<\/p>\n","protected":false},"author":3,"featured_media":107,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-106","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/106","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/comments?post=106"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/106\/revisions"}],"predecessor-version":[{"id":108,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/106\/revisions\/108"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media\/107"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media?parent=106"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/categories?post=106"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/tags?post=106"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}