OAuth 2.0 er en åpen autorisasjonsprotokoll definert i RFC 6749 som lar brukere gi tredjepartsapplikasjoner begrenset tilgang til ressurser uten å dele passord. Protokollen driver i dag innlogging med Google, GitHub, Microsoft og hundrevis av andre tjenester. Over 1 milliard brukere benytter daglig tjenester bygget på OAuth 2.0 uten å tenke over det. I Node.js er protokollen spesielt verdifull fordi du kan la eksisterende identitetsleverandører håndtere autentisering, redusere angrepsflaten og oppfylle GDPR-kravene i Norge uten å lagre passord selv.
Denne veiledningen tar deg gjennom 12 konkrete steg fra tomt prosjekt til et fungerende OAuth 2.0-system i Node.js. Du bygger en Express-applikasjon som bruker Authorization Code Flow med PKCE (Proof Key for Code Exchange), validerer state-parametre mot CSRF, bytter autorisasjonskoder mot access tokens, og fornyer tokens automatisk. Alle kodeeksempler er kjørbare, alle fallgruver er dokumenterte, og du får en komplett feilsøkingstabell med 8 vanlige feilmeldinger.
Hva er OAuth 2.0 og hva brukes det til?
OAuth 2.0 er en autorisasjonsprotokoll, ikke en autentiseringsprotokoll. Forskjellen er kritisk: OAuth 2.0 gir en applikasjon tilgang til ressurser på vegne av brukeren, mens autentisering handler om å bekrefte hvem brukeren faktisk er. Vil du legge til “Logg inn med Google”-funksjonalitet, bruker du OAuth 2.0 som transport og OpenID Connect (OIDC) som autentiseringslag oppå, som beskrevet i OpenID Connect-spesifikasjonen.
De fire aktørene i en OAuth 2.0-transaksjon er ressurseieren (brukeren), klientapplikasjonen (din Node.js-app), autorisasjonsserveren (for eksempel Google) og ressursserveren (API-et som eier dataene). Flyten ser slik ut: klienten sender brukeren til autorisasjonsserveren, brukeren godkjenner tilgang, autorisasjonsserveren returnerer en engangskode, klienten bytter koden mot et access token, og access tokenet brukes til å hente beskyttede ressurser.
I Node.js-sammenheng er OAuth 2.0 den anbefalte måten å integrere SSO (Single Sign-On) i bedriftsapplikasjoner. Du unngår å lagre brukerpassord i databasen, reduserer risikoen for credential-lekkasje, og begrenser tilgang via scopes. For norske virksomheter underlagt NIS2 og GDPR er dette en praktisk måte å oppfylle prinsippet om dataminimering: du ber bare om de tillatelsene du faktisk trenger.
Typiske brukstilfeller i Node.js-applikasjoner inkluderer: “Logg inn med Google/GitHub/Microsoft”-knapper i nettapplikasjoner, tilgang til tredjeparts API-er (Google Drive, GitHub-repositorier, Microsoft Graph) på vegne av brukere, og maskin-til-maskin (M2M) kommunikasjon mellom interne tjenester via Client Credentials Flow.
OAuth 2.0-flyter: Velg riktig for din applikasjon
OAuth 2.0 definerer fire autorisasjonsflyter (grant types). For webapplikasjoner med en server-side komponent er Authorization Code Flow med PKCE det eneste anbefalte valget per OAuth 2.0 Security Best Current Practice (RFC 9700, 2025). De andre flytene har kjente svakheter eller er ment for svært spesifikke brukstilfeller.
| Flyt (Grant Type) | Brukstilfelle | PKCE | Client Secret | Anbefalt 2026 |
|---|---|---|---|---|
| Authorization Code + PKCE | Webapper, mobilapper, SPA-er | Ja (obligatorisk) | Valgfritt | Ja |
| Client Credentials | Server-til-server (M2M) | Nei | Ja (obligatorisk) | Ja (kun M2M) |
| Device Authorization | TV-er, CLI-verktøy, IoT | Nei | Nei | Ja (kun enheter) |
| Implicit Flow | Eldre SPA-er (avviklet) | Nei | Nei | Nei (frarådes) |
| Resource Owner Password | Migrasjon fra legacy-systemer | Nei | Ja | Nei (frarådes sterkt) |
Authorization Code Flow med PKCE (definert i RFC 7636) løser to kjente angrep: autorisasjonskode-avskjæring og login CSRF. PKCE genererer et tilfeldig code_verifier-token (43-128 tegn per RFC 7636), beregner en SHA-256-hash av dette (code_challenge), og sender hashen med autorisasjonsforespørselen. På tokenutvekslingen sendes den opprinnelige code_verifier for verifisering. En angriper som snappes opp koden fra URL-en, mangler code_verifier og kan ikke fullføre flyten.
Implicit Flow returnerer access tokenet direkte i URL-fragmentet etter innlogging, noe som betyr at tokenet eksponeres i nettleserhistorikk, server-logger og Referer-headere. Denne flyten er formelt avviklet i OAuth 2.0 Security BCP (2025) og bør aldri brukes i nye implementasjoner.
Forutsetninger og versjoner
Denne veiledningen krever følgende programvare og versjoner:
- Node.js 20.x LTS eller nyere (Node.js 22.x LTS anbefales for produksjon i 2026). Sjekk med
node --version. - npm 10.x (følger med Node.js 22.x). Sjekk med
npm --version. - Express 4.x for HTTP-ruting og mellomvare
- simple-oauth2 5.x for OAuth 2.0 tokenbehandling
- express-session 1.x for server-side øktlagring
- dotenv 16.x for miljøvariabler
- En OAuth 2.0-leverandørkonto: Google Cloud Console (gratis), GitHub (gratis), eller Microsoft Entra
- Grunnleggende kjennskap til asynkron JavaScript, Express-ruter og HTTP
Node.js 20.x eller nyere er nødvendig fordi node:crypto-modulen fra Node.js 20 inkluderer stabil Web Crypto API-støtte og den innebygde fetch-funksjonen er tilgjengelig fra Node.js 18. Eldre versjoner av Node.js mangler stabil base64url-kodingsstøtte og innebygd fetch, som begge brukes i denne veiledningen. Les mer om den innebygde kryptomodulen i HTTPS og TLS 1.3 i Node.js.
Steg 1: Opprett prosjektstruktur
En ryddig katalogstruktur skiller konfigurasjons-, mellomvare- og rutelogikk fra hverandre, noe som forenkler testing og vedlikehold. Opprett prosjektet med følgende struktur:
mkdir oauth-demo && cd oauth-demo
npm init -y
# Opprett katalogstruktur
mkdir -p src/{routes,middleware,utils}
touch src/index.js
touch src/routes/auth.js
touch src/middleware/tokenGuard.js
touch src/utils/pkce.js
touch .env
touch .env.example
touch .gitignore
Legg til følgende i .gitignore umiddelbart, før første commit:
node_modules/
.env
*.log
.DS_Store
.env-filen inneholder client_secret og SESSION_SECRET og skal aldri commites til versjonskontroll. GitHub scanner daglig offentlige repositorier for eksponerte OAuth-hemmeligheter og varsler leverandører om å tilbakekalle dem. En eksponert client_secret kan misbrukes til å utstede tokens og få tilgang til alle brukernes ressurser i applikasjonen.
Steg 2: Installer avhengigheter
Installer de nødvendige pakkene. Vi bruker et minimalt sett avhengigheter for å holde angrepsflaten lav og gi full forståelse av hva som skjer under overflaten.
npm install express simple-oauth2 express-session dotenv
# Utviklingsavhengigheter
npm install --save-dev nodemon
# Sjekk installerte versjoner
npm list --depth=0
Pakkenes formål:
- express: HTTP-rammeverk for ruting og mellomvare. Håndterer innkommende forespørsler fra nettleseren.
- simple-oauth2: Håndterer token-forespørsler og fornyelse mot OAuth 2.0-leverandørens API. Abstrahere HTTP-kallene mot token-endepunktet.
- express-session: Lagrer state-parameteren og code_verifier mellom autorisasjonsforespørselen og callbacken. Avgjørende for PKCE og CSRF-beskyttelse.
- dotenv: Laster miljøvariabler fra
.env-filen uten at de hardkodes i kildekode.
Vi bruker ikke passport.js med vilje. Passport er et nyttig abstraksjonslag, men det skjuler OAuth-flyten og gjør feilsøking vanskeligere for de som lærer protokollen. Å bygge flyten eksplisitt gir full forståelse og gjør det enklere å tilpasse til organisasjonens egne identitetsløsninger eller ikke-standard leverandøre.
Kjør npm audit etter installasjon for å sjekke om avhengighetene har kjente sårbarheter. For et dypere dykk i avhengighetssikkerhet, se npm audit fix i Node.js.
Steg 3: Konfigurer miljøvariabler og registrer applikasjonen
Før du kan skrive kode, må applikasjonen registreres hos OAuth 2.0-leverandøren. Prosessen er lik for de fleste leverandører:
Google Cloud Console: Gå til console.cloud.google.com, velg eller opprett et prosjekt, gå til API & Services, Credentials, Create Credentials, OAuth 2.0 Client IDs. Velg “Web application”. Under “Authorized redirect URIs” legger du til http://localhost:3000/auth/callback for utvikling. Du får Client ID og Client Secret.
GitHub: Gå til github.com/settings/applications/new. Sett “Authorization callback URL” til http://localhost:3000/auth/callback. GitHub bruker https://github.com/login/oauth/authorize som authorization URL og https://github.com/login/oauth/access_token som token URL.
Fyll inn .env-filen (eksempel med Google):
# OAuth 2.0 leverandørkonfigurasjon (Google-eksempel)
OAUTH_CLIENT_ID=din-client-id.apps.googleusercontent.com
OAUTH_CLIENT_SECRET=GOCSPX-din-client-secret
# Endepunkter (bytt ut for andre leverandører)
OAUTH_TOKEN_HOST=https://oauth2.googleapis.com
OAUTH_TOKEN_PATH=/token
OAUTH_AUTH_URL=https://accounts.google.com/o/oauth2/v2/auth
OAUTH_REVOKE_URL=https://oauth2.googleapis.com/revoke
# Applikasjonskonfigurasjon
OAUTH_REDIRECT_URI=http://localhost:3000/auth/callback
OAUTH_SCOPE=openid email profile
# Express-konfigurasjon
SESSION_SECRET=generer-med-crypto-randomBytes-32-hex
PORT=3000
NODE_ENV=development
Generer en sikker SESSION_SECRET med Node.js:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
# Eksempel output:
# a3f8b2c9d4e1f0a7b8c9d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3
Bruk aldri en kort, statisk eller forutsigbar SESSION_SECRET i produksjon. Denne verdien sikrer signering av økt-cookies. Et svakt SESSION_SECRET gjør at angripere kan forfalske økt-cookies og utgi seg for andre brukere. Kopier .env til .env.example med tomme verdier for teamdeling.
Steg 4: Konfigurer Express-server med øktbehandling
Øktbehandling er kritisk for OAuth 2.0-flyten. Applikasjonen må lagre state-parameteren og code_verifier mellom autorisasjonsforespørselen og callbacken. Uten økt kan applikasjonen ikke verifisere at callbacken tilhører samme bruker som startet flyten.
// src/index.js
'use strict';
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const authRoutes = require('./routes/auth');
const tokenGuard = require('./middleware/tokenGuard');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Øktbehandling - MUST settes opp før ruter
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Blokkerer JavaScript-tilgang til cookie (XSS-beskyttelse)
secure: process.env.NODE_ENV === 'production', // Krev HTTPS i produksjon
sameSite: 'lax', // CSRF-beskyttelse for normale navigasjoner
maxAge: 10 * 60 * 1000 // 10 minutter (tilstrekkelig for auth-flyten)
}
}));
app.use('/auth', authRoutes);
// Eksempel på en offentlig rute
app.get('/', (req, res) => {
if (req.session.accessToken) {
return res.json({
status: 'innlogget',
bruker: req.session.userInfo || null
});
}
res.json({
status: 'ikke innlogget',
loggInn: '/auth/login'
});
});
// Eksempel på beskyttet rute
app.get('/profil', tokenGuard, async (req, res) => {
try {
const svar = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${req.session.accessToken}` }
});
if (!svar.ok) {
return res.status(svar.status).json({ feil: 'Klarte ikke hente brukerinfo' });
}
const brukerInfo = await svar.json();
req.session.userInfo = brukerInfo;
res.json(brukerInfo);
} catch (err) {
res.status(500).json({ feil: err.message });
}
});
app.listen(PORT, () => {
console.log(`OAuth 2.0-server kjører på http://localhost:${PORT}`);
console.log(`Logg inn på: http://localhost:${PORT}/auth/login`);
});
Cookie-innstillingene er nøye gjennomtenkte. httpOnly: true hindrer JavaScript fra å lese økt-cookies, noe som blokkerer XSS-angrep som forsøker å stjele øktdata. secure: true i produksjon krever HTTPS for cookieoverføring, slik at cookies ikke sendes over usikrede HTTP-tilkoblinger. sameSite: 'lax' gir grunnleggende CSRF-beskyttelse ved å hindre at cookies sendes med kryssdomene-forespørsler initiert av tredjepartssider. For sikkerhetshoder som CSP og HSTS, se veiledningen om Helmet.js i Node.js.
Steg 5: Implementer PKCE code_verifier og code_challenge
PKCE (Proof Key for Code Exchange, RFC 7636) er nå obligatorisk for alle offentlige klienter og sterkt anbefalt for konfidensielle klienter per OAuth 2.0 Security BCP. Implementer begge funksjonene i en dedikert hjelpefil ved hjelp av Node.js sin innebygde node:crypto-modul.
// src/utils/pkce.js
'use strict';
const crypto = require('node:crypto');
/**
* Genererer en kryptografisk sikker code_verifier.
* Lengde: 43–128 tegn (URL-sikker base64), per RFC 7636 §4.1.
* Bruker 64 tilfeldige bytes = 86 base64url-tegn (avkortet til 128).
*/
function genererCodeVerifier() {
return crypto
.randomBytes(64)
.toString('base64url')
.slice(0, 128);
}
/**
* Beregner SHA-256 code_challenge fra code_verifier.
* Returnerer base64url-kodet hash uten padding, per RFC 7636 §4.2.
* Merk: base64url er forskjellig fra standard base64 (ingen +/=/- tegn).
*/
function genererCodeChallenge(verifier) {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
/**
* Genererer en kryptografisk sikker state-verdi for CSRF-beskyttelse.
* Minimum 32 bytes entropi anbefalt av OAuth Security BCP (RFC 9700).
*/
function genererState() {
return crypto.randomBytes(32).toString('base64url');
}
module.exports = { genererCodeVerifier, genererCodeChallenge, genererState };
Eksempel på hva funksjonene produserer:
// Eksempel output (verdier er tilfeldige ved hvert kall)
const verifier = genererCodeVerifier();
// verifier (128 tegn, URL-sikre tegn a-z A-Z 0-9 - _ ~):
// "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk9icMWe7..."
const challenge = genererCodeChallenge(verifier);
// challenge (43 tegn, SHA-256 hash av verifier i base64url):
// "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
const state = genererState();
// state (43 tegn, tilfeldig kryptografisk verdi):
// "Tzm4KD9y3-xQIzPdvJ8n2hLgRoNe5qWsBCfYUAHu0v1"
base64url-enkodingen (uten padding =) er spesifikk for RFC 7636. Vanlig base64 bruker +, / og = som ikke er URL-sikre og vil bryte autorisasjonsforespørselen. Node.js 20+ støtter base64url direkte som argument i toString() og digest() uten ekstra transformasjoner, noe som eliminerer en vanlig implementasjonsfeil. Den innebygde Node.js kryptografien er bygget på OpenSSL og er beskrevet i den offisielle Node.js Crypto-dokumentasjonen.
Steg 6: Bygg autorisasjonslenke og omdiriger bruker
Autorisasjonslenken sender brukeren til leverandørens innloggingsside med alle nødvendige parametre. Hvert parameter har en spesifikk sikkerhetsrolle. Feil parameter-kombinasjon er den vanligste kilden til OAuth 2.0-integrasjonsproblemer.
// src/routes/auth.js
'use strict';
const express = require('express');
const { AuthorizationCode } = require('simple-oauth2');
const { genererCodeVerifier, genererCodeChallenge, genererState } = require('../utils/pkce');
const router = express.Router();
// Opprett OAuth 2.0-klient (Google-konfigurasjon)
function lagOauthKlient() {
return new AuthorizationCode({
client: {
id: process.env.OAUTH_CLIENT_ID,
secret: process.env.OAUTH_CLIENT_SECRET
},
auth: {
tokenHost: process.env.OAUTH_TOKEN_HOST,
tokenPath: process.env.OAUTH_TOKEN_PATH,
authorizePath: process.env.OAUTH_AUTH_URL
}
});
}
// GET /auth/login - start OAuth 2.0 Authorization Code Flow med PKCE
router.get('/login', (req, res) => {
// Generer PKCE-verdier for denne flyten
const codeVerifier = genererCodeVerifier();
const codeChallenge = genererCodeChallenge(codeVerifier);
const state = genererState();
// Lagre i økt - brukes til verifisering i callback
req.session.oauthState = state;
req.session.codeVerifier = codeVerifier;
const oauthKlient = lagOauthKlient();
const autorisasjonsURL = oauthKlient.authorizeURL({
redirect_uri: process.env.OAUTH_REDIRECT_URI,
scope: process.env.OAUTH_SCOPE,
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
console.log('Omdirigerer til autorisasjonsserver...');
res.redirect(autorisasjonsURL);
});
Parametrene i autorisasjons-URL-en har disse sikkerhetsrollene:
| Parameter | Eksempelverdi | Sikkerhetsformål |
|---|---|---|
| response_type | code | Ber om autorisasjonskode, ikke token direkte (hindrer Implicit Flow-angrep) |
| client_id | app-id.apps.googleusercontent.com | Identifiserer applikasjonen overfor leverandøren |
| redirect_uri | http://localhost:3000/auth/callback | Må matche eksakt med registrert URI (forhindrer token-tyveri via omdirigering) |
| scope | openid email profile | Dataminimering: begrenser tilgang til angitte ressurser |
| state | Tilfeldig 43-tegns verdi | CSRF-beskyttelse: verifiseres i callback mot økt-verdi |
| code_challenge | SHA-256 hash av code_verifier | PKCE: hindrer autorisasjonskodekapring |
| code_challenge_method | S256 | Angir SHA-256 som hashalgoritme (plain er ikke sikker) |
Steg 7: Håndter OAuth-callback og valider state
Callbacken er det mest kritiske punktet i flyten. Her mottar applikasjonen autorisasjonskoden fra leverandøren. Valider alltid state-parameteren mot økt-verdien før koden brukes. Manglende state-validering er en kjent CSRF-sårbarhet i OAuth-implementasjoner (se OWASP OAuth 2.0 Threat Model).
// Fortsettelse av src/routes/auth.js
// GET /auth/callback - motta autorisasjonskode og bytt mot tokens
router.get('/callback', async (req, res) => {
const { code, state, error, error_description } = req.query;
// Håndter eksplisitt feil fra leverandøren (f.eks. bruker klikket "Avbryt")
if (error) {
console.error(`OAuth-leverandørfeil: ${error} - ${error_description}`);
return res.status(400).json({
feil: error,
beskrivelse: error_description || 'Autorisasjon avvist av leverandøren'
});
}
// Kritisk: Valider state mot økt-verdi (CSRF-beskyttelse)
if (!state || !req.session.oauthState || state !== req.session.oauthState) {
console.warn('State-validering feilet - mulig CSRF-angrep');
return res.status(403).json({
feil: 'Ugyldig state-parameter',
beskrivelse: 'Sikkerhetsverifisering feilet. Start innlogging på nytt.'
});
}
if (!code) {
return res.status(400).json({ feil: 'Mangler autorisasjonskode i callback' });
}
// Hent code_verifier fra økt (brukes til PKCE-verifisering)
const codeVerifier = req.session.codeVerifier;
// Rens sensitiv øktdata etter bruk (hindrer gjenbruksangrep)
delete req.session.oauthState;
delete req.session.codeVerifier;
try {
const oauthKlient = lagOauthKlient();
const tokenResponse = await oauthKlient.getToken({
code: code,
redirect_uri: process.env.OAUTH_REDIRECT_URI,
code_verifier: codeVerifier // PKCE: sendes kun her, aldri til nettleser
});
const token = tokenResponse.token;
// Lagre tokens og utløpstidspunkt i økt (aldri i cookie direkte)
req.session.accessToken = token.access_token;
req.session.refreshToken = token.refresh_token || null;
req.session.tokenExpiry = Date.now() + ((token.expires_in || 3600) * 1000);
req.session.tokenType = token.token_type || 'Bearer';
console.log('Token-utveksling vellykket. Omdirigerer...');
res.redirect('/');
} catch (err) {
console.error('Token-utvekslingsfeil:', err.message);
res.status(500).json({
feil: 'Token-utveksling feilet',
detalj: process.env.NODE_ENV === 'development' ? err.message : 'Intern feil'
});
}
});
State-verdien slettes fra økt etter én verifisering (delete req.session.oauthState) for å hindre gjenbruk av samme state-verdi i nye forespørsler. code_verifier sendes kun i token-forespørselen til leverandørens server, aldri til nettleseren eller i URL-parametre. Autorisasjonskoder er gyldige i kun 30-60 sekunder og kan bare brukes én gang per RFC 6749.
Steg 8: Implementer tokenvalidering som mellomvare
Sentralisert tokenvalidering i en mellomvarefunksjon sikrer at all beskyttet rutelogikk håndterer utløpte tokens konsistent. Mellomvaren fornyer automatisk tokens som utløper innen 60 sekunder for å hindre avbrudd midt i brukerens arbeid.
// src/middleware/tokenGuard.js
'use strict';
const { AuthorizationCode } = require('simple-oauth2');
async function tokenGuard(req, res, next) {
// Sjekk om bruker har et aktivt access token i økt
if (!req.session.accessToken) {
return res.status(401).json({
feil: 'Ikke autentisert',
loggInn: '/auth/login'
});
}
// Proaktiv fornyelse: Fornye token hvis det utløper innen 60 sekunder
const msIgjen = (req.session.tokenExpiry || 0) - Date.now();
if (msIgjen < 60_000 && req.session.refreshToken) {
try {
await fornyToken(req);
console.log('Access token fornyet proaktivt');
} catch (err) {
console.error('Token-fornyelse feilet:', err.message);
// Økt er ugyldig - krev ny innlogging
req.session.destroy(() => {});
return res.status(401).json({
feil: 'Sesjon utløpt. Vennligst logg inn på nytt.',
loggInn: '/auth/login'
});
}
}
next();
}
async function fornyToken(req) {
const oauthKlient = new AuthorizationCode({
client: {
id: process.env.OAUTH_CLIENT_ID,
secret: process.env.OAUTH_CLIENT_SECRET
},
auth: {
tokenHost: process.env.OAUTH_TOKEN_HOST,
tokenPath: process.env.OAUTH_TOKEN_PATH
}
});
const eksisterendeToken = oauthKlient.createToken({
access_token: req.session.accessToken,
refresh_token: req.session.refreshToken,
token_type: req.session.tokenType || 'Bearer',
expires_in: 0 // Tvinger fornyelse
});
const fornyet = await eksisterendeToken.refresh();
// Oppdater økt med nye token-verdier
req.session.accessToken = fornyet.token.access_token;
req.session.tokenExpiry = Date.now() + ((fornyet.token.expires_in || 3600) * 1000);
// Google roterer ikke refresh tokens som standard, men oppdater ved behov
if (fornyet.token.refresh_token) {
req.session.refreshToken = fornyet.token.refresh_token;
}
}
module.exports = tokenGuard;
Access tokenet sendes alltid i Authorization: Bearer-headeren til ressursservere, aldri som URL-parameter. URL-parametre logges av webservere, deles via Referer-header til tredjeparter, og vises i nettleserhistorikk. Dette er et krav etter RFC 6750.
Steg 9: Implementer utlogging og tokenopphevelse
Utlogging i OAuth 2.0 er todelt. Lokal utlogging rydder øktdata i applikasjonen. Tokenopphevelse informerer leverandøren om at token ikke lenger er gyldig. Mange applikasjoner implementerer bare lokal utlogging, noe som er en sikkerhetssvakhet: et stjålet token forblir gyldig hos leverandøren inntil det utløper naturlig (typisk 3600 sekunder).
// Legg til i src/routes/auth.js
// GET /auth/loggut - opphev token og rydd lokal økt
router.get('/loggut', async (req, res) => {
const accessToken = req.session.accessToken;
const refreshToken = req.session.refreshToken;
// Rydd lokal økt umiddelbart (uavhengig av tokenopphevelse)
await new Promise((resolve) => req.session.destroy(resolve));
// Ryd cookie
res.clearCookie('connect.sid');
// Tilbakekall access token hos leverandøren (RFC 7009)
if (accessToken && process.env.OAUTH_REVOKE_URL) {
try {
const revokeResponse = await fetch(process.env.OAUTH_REVOKE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ token: accessToken }).toString()
});
if (!revokeResponse.ok) {
console.warn('Tokenopphevelse rapporterte feil:', revokeResponse.status);
} else {
console.log('Access token tilbakekalt hos leverandøren');
}
} catch (err) {
// Logg feil men blokker ikke utloggingen - lokal økt er allerede slettet
console.error('Tilbakekallingsfeil (ikke blokkerende):', err.message);
}
}
res.redirect('/');
});
module.exports = router;
Tokenopphevelse (RFC 7009) er spesielt viktig i tre scenarier: brukere som logger ut fra offentlige eller delte datamaskiner, mistenkt konto-kompromittering der alle aktive tokens bør ugyldiggjøres, og bedriftsapplikasjoner der tilbakekalt organisasjonstilgang skal tre i kraft umiddelbart. Revoke-endepunktet aksepterer enten access token eller refresh token.
Steg 10: Håndter token-levetider og stille fornyelse
Google OAuth 2.0 access tokens utløper etter 3600 sekunder (1 time). Uten automatisk fornyelse opplever brukeren at sesjonen “dør” midt i arbeidsflyten. Token-mellomvaren i steg 8 håndterer proaktiv fornyelse, men det er viktig å forstå levetidene:
| Token-type | Typisk levetid (Google) | Fornyes via | Oppheves ved |
|---|---|---|---|
| Authorization Code | 30–60 sekunder | Ikke fornyes (engangsbruk) | Automatisk etter bruk |
| Access Token | 3600 sekunder (1 time) | Refresh Token | Revoke-endepunkt eller utløp |
| Refresh Token (Google, standard) | Ubegrenset (inntil tilgang tilbakekalles) | Ikke fornyes direkte | Brukertilgang tilbakekalles |
| Refresh Token (Google, offline) | Utløper etter 6 måneder uten bruk | Ikke fornyes | Inaktivitet, tilbakekalling |
| ID Token (OpenID Connect) | 3600 sekunder | Ny autorisasjonsflyt | Ikke aktuelt (bruk lokalt) |
For produksjonsapplikasjoner med lang levetid (dager eller uker) bør du be om scope=offline_access (eller tilsvarende for din leverandør) for å motta et refresh token. Noen leverandøre (inkludert Google) utsteder ikke refresh token som standard for web-klienter, med mindre brukeren eksplisitt gir “offline access”.
Steg 11: Konfigurer for produksjon
Standard express-session bruker minnebasert lagring som ikke skalerer, ikke overlever omstart, og er ikke egnet for produksjon. Bruk Redis som øktlager for produksjonsmiljøer.
# Installer Redis-adapter for øktlagring i produksjon
npm install connect-redis redis
// Produksjonskonfigurasjon - legg til i src/index.js
// (betinget basert på NODE_ENV)
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
let sessionStore;
if (process.env.NODE_ENV === 'production') {
const redisKlient = createClient({ url: process.env.REDIS_URL });
redisKlient.connect().catch((err) => {
console.error('Redis-tilkoblingsfeil:', err);
process.exit(1);
});
sessionStore = new RedisStore({ client: redisKlient, prefix: 'oauth:' });
}
// Oppdatert session-konfigurasjon
app.use(session({
store: sessionStore, // undefined i utvikling = minnebasert lagring
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
name: '__Host-sid', // __Host-prefiks krever Secure + Path=/ + ingen Domain (ekstra sikkerhet)
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict', // Strengere CSRF-beskyttelse i produksjon
maxAge: 24 * 60 * 60 * 1000 // 24 timer
}
}));
// Aktiver trust proxy hvis appen kjører bak Nginx eller lastbalanserer
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
trust proxy-innstillingen er nødvendig bak lastbalanserere (Nginx, AWS ALB, Cloudflare). Uten den vil Express se intern IP fra lastbalansereren, og secure: true-cookie-innstillingen vil avvise cookies fordi forbindelsen fra lastbalanserer til Node.js internt er HTTP. For en komplett guide til Node.js-sikkerhet med Helmet.js, se Helmet.js og sikkerhetshoder i Node.js.
Steg 12: Test det komplette systemet
Start serveren og test flyten manuelt. Verifiser hvert steg i flyten for å bekrefte at PKCE, state-validering og token-utveksling fungerer korrekt.
# Start serveren
node src/index.js
# Med automatisk restart ved kodeendringer:
npx nodemon src/index.js
# Forventet oppstartsoutput:
# OAuth 2.0-server kjører på http://localhost:3000
# Logg inn på: http://localhost:3000/auth/login
# Test 1: Sjekk ikke-autentisert tilstand
curl http://localhost:3000/
# Forventet:
# {"status":"ikke innlogget","loggInn":"/auth/login"}
# Test 2: Verifiser at beskyttet rute krever autentisering
curl http://localhost:3000/profil
# Forventet:
# {"feil":"Ikke autentisert","loggInn":"/auth/login"}
# Test 3: Start innloggingsflyten i nettleser
# Åpne: http://localhost:3000/auth/login
# Du omdirigeres til Googles innloggingsside
Åpne http://localhost:3000/auth/login i nettleseren. Nettleseren omdirigeres til Googles innloggingsside. Sjekk URL-en i adressefeltet for å bekrefte at code_challenge og state er inkludert. Etter innlogging sendes du tilbake til /auth/callback. Besøk http://localhost:3000/profil for å se brukerinformasjon fra Google. Test utlogging på http://localhost:3000/auth/loggut og bekreft at /profil returnerer 401 etterpå.
For automatiserte tester: bruk oauth2-mock-server (npm-pakken) for å simulere en OAuth 2.0-autorisasjonsserver i testmiljøet. Du kan injisere feilscenarier (ugyldig kode, utløpt token) for å verifisere at feilhåndteringen fungerer som forventet uten å treffe ekte leverandør-API-er. SQL-injeksjonsangrep mot din applikasjon kan omgå autentisering hvis databasespørringer ikke er parameterisert – se SQL Injection i Node.js.
5 vanlige fallgruver og how to unngå dem
Disse feilene gjentar seg i nesten alle OAuth 2.0-integrasjoner. Mange er usynlige under utvikling men dukker opp som alvorlige sikkerhetssvakheter i produksjon eller under sikkerhetsrevisjoner.
Fallgruve 1: Client secret i kildekode eller Git-historikk. Å hardkode OAUTH_CLIENT_SECRET direkte i kildekode er den vanligste kilden til kompromitterte OAuth-integrasjoner. GitHub-søk avslører daglig tusenvis av aktive OAuth-hemmeligheter i offentlige repositorier. Hemmeligheten i Git-historikken fjernes ikke ved å slette filen, da historikken må reskrives med verktøy som git-filter-repo. Bruk alltid miljøvariabler, legg .env i .gitignore fra aller første commit, og roter hemmeligheten umiddelbart hvis den er eksponert.
Fallgruve 2: Bruke Implicit Flow i stedet for Authorization Code + PKCE. Implicit Flow returnerer access tokenet direkte i URL-fragmentet (#access_token=...) etter innlogging. Dette betyr at tokenet er synlig i nettleserhistorikk, server-logger for redirect-URL-er, og Referer-headere til tredjepartsskript på siden. Implicit Flow er formelt avviklet i OAuth 2.0 Security BCP og Google, Microsoft og GitHub fraråder det aktivt. Bruk alltid Authorization Code Flow med PKCE, selv for Single Page Applications som ikke kan lagre client_secret.
Fallgruve 3: Ikke validere state-parameteren i callback. Uten state-validering er applikasjonen sårbar for login CSRF-angrep. En angriper kan konstruere en URL som fullfører autentisering med angriperens konto, slik at offeret logges inn som angriperen (og angriperens handlinger i applikasjonen knyttes til offerets konto). State-verdien skal: genereres kryptografisk tilfeldig, lagres i server-side økt (ikke cookie), og sammenlignes bit-for-bit (ikke substring-sammenligning) i callback. Se steg 7 for korrekt implementasjon.
Fallgruve 4: Sende access token i URL-parametre. En forespørsel som GET /api/data?token=ACCESS_TOKEN eksponerer tokenet i server-logger, nettleserhistorikk, og Referer-header til tredjeparter. RFC 6750 krever at access tokens sendes i Authorization: Bearer TOKEN-headeren. Aldri send tokens som URL-parametre, og aldri logg Authorization-headere eller request-bodies som inneholder tokens i produksjonslogger.
Fallgruve 5: Lagre tokens i localStorage eller sessionStorage. Tokens i localStorage er tilgjengelige fra all JavaScript på siden, inkludert injisert skadelig kode fra XSS-angrep eller kompromitterte tredjepartsskript. For server-rendret Node.js-applikasjoner: lagre tokens eksklusivt i server-side økt. For SPA-er med Node.js-backend: la backend håndtere alle tokens og eksponer kun en httpOnly-cookie til frontend, ikke selve tokenverdien. Passord og sensitive data i nettleserlagring er en av de vanligste XSS-konsekvensene.
Feilsøkingstabell: 8 vanlige OAuth 2.0-feilmeldinger
| Feilmelding | Vanligste årsak | Løsning |
|---|---|---|
redirect_uri_mismatch | Callback-URL stemmer ikke eksakt med registrert URI hos leverandøren | Kopier OAUTH_REDIRECT_URI fra .env og sammenlign tegn-for-tegn med registrert URI i leverandørens konsoll. Inkludert port, protokoll (http vs https) og sti |
invalid_client | Feil client_id eller client_secret, eller secret er utløpt | Kopier verdiene direkte fra leverandørens konsoll. Sjekk at ingen mellomrom, linjeskift eller spesialtegn er inkludert i .env-verdien |
invalid_grant | Autorisasjonskoden er allerede brukt, utløpt (>60 sek), eller code_verifier stemmer ikke | Autorisasjonskoder er engangsbruk. Sjekk at code_verifier er lagret korrekt i økt og hentes riktig i callback. Start flyten på nytt |
access_denied | Brukeren avviste tilgangen eller klikket Avbryt | Omdiriger brukeren tilbake til applikasjonen med en forklarende melding. Ikke gjenta autorisasjonsforespørselen automatisk uten brukerhandling |
invalid_scope | Scope er ikke aktivert for applikasjonen eller er skrevet feil | Sjekk at scope er aktivert i leverandørens API-konsoll. For Google: aktiver relevante API-er i Cloud Console. Scope-verdier er mellomromseparerte, ikke kommaseparerte |
401 Unauthorized fra API | Access token er utløpt (etter 3600 sek) og ikke fornyet | Implementer proaktiv token-fornyelse via refresh token (se steg 8). Sjekk at tokenExpiry oppdateres korrekt etter fornyelse |
| CORS-feil ved token-forespørsler | Token-forespørsler sendes fra klient-JavaScript direkte til leverandørens token-endepunkt | Tokenutveksling og fornyelse skal alltid skje server-side i Node.js-backend. Aldri kall token-endepunkt direkte fra nettleser-JavaScript |
| Tom økt i callback (state er null) | Cookie satt med secure: true i utvikling (ingen HTTPS), eller SameSite-problemer, eller proxy striper cookies | Sett secure: false i utvikling. Sjekk at trust proxy er aktivert bak lastbalanserer. Verifiser cookie-flyt i nettleserens DevTools (Application > Cookies) |
Avanserte tips for produksjonsklare OAuth 2.0-systemer
Bruk PKCE for alle klienttyper, ikke bare offentlige klienter. PKCE ble opprinnelig designet for mobilapper og SPA-er uten mulighet til å lagre hemmeligheter, men RFC 9700 (2025) anbefaler det for alle klienttyper. Server-side webapper med client_secret drar nytte av PKCE som et ekstra forsvarslag mot autorisasjonskode-avskjæring i tilfeller der leverandørens kommunikasjonskanal er kompromittert.
Implementer Demonstrating Proof of Possession (DPoP) for høysikkerhetsapplikasjoner. DPoP (RFC 9449) binder access tokens kryptografisk til en spesifikk nøkkel kontrollert av klienten. Et stjålet DPoP-bundet token er ubrukelig uten den tilhørende private nøkkelen. Google og Microsoft Entra ID støtter DPoP. Implementasjon i Node.js krever generering av et Ed25519-nøkkelpar med node:crypto og signering av DPoP-bevis per forespørsel. Les mer om asymmetrisk kryptografi i veiledningen om scrypt Passordhashing i Node.js for relaterte kryptografiske konsepter.
Aktiver refresh token-rotasjon for oppdagelse av tokentyverier. Med refresh token-rotasjon ugyldiggjøres det gamle refresh tokenet hver gang det brukes til å hente et nytt access token. Hvis en angriper stjeler et refresh token og bruker det, vil det gyldige systemet oppdage at et allerede brukt token ble sendt på nytt, og autorisasjonsserveren vil ugyldiggjøre hele token-familien (RFC 6749 §10.4). Konfigurer rotasjon i leverandørens konsoll eller bruk separate biblioteker som støtter sender-constrained tokens.
Valider tokens med leverandørens JWKs-endepunkt for API-servere. Hvis Node.js-applikasjonen fungerer som en ressursserver (aksepterer tokens fra andre klienter), hent leverandørens JSON Web Key Set (JWKS) fra deres /.well-known/openid-configuration-endepunkt og valider tokens lokalt. Lokal JWT-validering er raskere enn token-introspeksjon (RFC 7662) per forespørsel, men krever caching av JWKS med regelmessig oppdatering (typisk hvert 24 time).
Logg OAuth-hendelser for revisjonsformål. For norske virksomheter underlagt GDPR og NIS2 er hendelseslogging av autentisering ikke bare god praksis, det kan være påkrevd. Logg: innloggingsforsøk (suksess og feil) med tidsstempler og kilde-IP, token-utstedelser og fornyelser, og tilbakekallingen med årsak. Unngå å logge selve token-verdiene. Bruk strukturert logging (JSON) for enkel integrasjon med SIEM-systemer.
Implementer rate limiting på OAuth-endepunkter. Autorisasjons- og token-endepunktene dine bør beskyttes mot brute force og scraping. En angriper som bombarderer /auth/callback med falske state-verdier kan provosere frem informasjonslekkasje i feilmeldinger. Bruk express-rate-limit for å begrense antall forespørsler per IP. For en komplett guide til rate limiting, se veiledningen om Rate Limiting i Node.js.
Siste nytt
CybersikkerhetSopra Steria 2026: Norden i Verste Cybertrussel Siden 2. VerdenskrigJun 28, 2026
CybersikkerhetUnit 42 2026: 89 % av Brudd via Identitet, Angrep 4x RaskereJun 28, 2026
CybersikkerhetSnort 3 vs Suricata 7: 21% vs 33% CPU, Gratis IDS [2026]Jun 28, 2026
CybersikkerhetpfSense vs OPNsense: Gratis vs $129/år, 80+ Plugins [2026]Jun 28, 2026