正在加载,请稍候…

OAuth 2.0 and OpenID Connect: Complete Implementation Guide

Master OAuth 2.0 and OIDC for secure authentication. Learn authorization code flow, PKCE, refresh tokens, JWT validation, and secure implementation patterns.

OAuth 2.0 and OpenID Connect

OAuth 2.0 Authorization Code Flow with PKCE

import crypto from 'crypto'

// Step 1: Generate PKCE code verifier and challenge
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url')
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
  return { verifier, challenge }
}

// Step 2: Build authorization URL
function buildAuthURL(config, codeChallenge, state) {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: config.clientId,
    redirect_uri: config.redirectUri,
    scope: 'openid profile email',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  })
  return `${config.authorizationEndpoint}?${params}`
}

// Step 3: Exchange code for tokens
async function exchangeCode(code, codeVerifier, config) {
  const resp = await fetch(config.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: config.clientId,
      redirect_uri: config.redirectUri,
      code,
      code_verifier: codeVerifier,
    }),
  })
  return resp.json()
}

JWT Validation

import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxEntries: 5,
  cacheMaxAge: 600000, // 10 minutes
})

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) return callback(err)
    callback(null, key.getPublicKey())
  })
}

function validateToken(token, options = {}) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, getKey, {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
      audience: process.env.CLIENT_ID,
      ...options,
    }, (err, decoded) => {
      if (err) reject(err)
      else resolve(decoded)
    })
  })
}

Refresh Token Rotation

class TokenManager {
  constructor(config) {
    this.config = config
    this.tokens = null
  }

  async getAccessToken() {
    if (!this.tokens) throw new Error('Not authenticated')
    if (this.isExpired()) {
      await this.refresh()
    }
    return this.tokens.access_token
  }

  isExpired() {
    return Date.now() >= this.tokens.expires_at - 60000 // 1 min buffer
  }

  async refresh() {
    const resp = await fetch(this.config.tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: this.config.clientId,
        refresh_token: this.tokens.refresh_token,
      }),
    })

    if (!resp.ok) {
      this.tokens = null // Force re-login
      throw new Error('Token refresh failed')
    }

    const data = await resp.json()
    this.tokens = {
      ...data,
      expires_at: Date.now() + data.expires_in * 1000,
    }
  }
}

Express.js OAuth Middleware

import express from 'express'
import session from 'express-session'
import { Issuer, generators } from 'openid-client'

const app = express()
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }))

let client
async function setupOIDC() {
  const issuer = await Issuer.discover('https://accounts.google.com')
  client = new issuer.Client({
    client_id: process.env.GOOGLE_CLIENT_ID,
    client_secret: process.env.GOOGLE_CLIENT_SECRET,
    redirect_uris: ['http://localhost:3000/callback'],
    response_types: ['code'],
  })
}

app.get('/login', (req, res) => {
  const state = generators.state()
  const nonce = generators.nonce()
  const codeVerifier = generators.codeVerifier()
  const codeChallenge = generators.codeChallenge(codeVerifier)

  req.session.auth = { state, nonce, codeVerifier }

  const authUrl = client.authorizationUrl({
    scope: 'openid email profile',
    state, nonce, code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  })

  res.redirect(authUrl)
})

app.get('/callback', async (req, res) => {
  const params = client.callbackParams(req)
  const { state, nonce, codeVerifier } = req.session.auth

  const tokenSet = await client.callback(
    'http://localhost:3000/callback',
    params, { state, nonce, code_verifier: codeVerifier }
  )

  const userinfo = await client.userinfo(tokenSet.access_token)
  req.session.user = userinfo
  res.redirect('/dashboard')
})

// Protected route middleware
function requireAuth(req, res, next) {
  if (!req.session.user) return res.redirect('/login')
  next()
}

app.get('/dashboard', requireAuth, (req, res) => {
  res.json({ user: req.session.user })
})

Security Best Practices

Concern Mitigation
CSRF State parameter + SameSite cookies
Token theft Short-lived access tokens (15 min)
Refresh token reuse Token rotation + detect reuse
Redirect URI Exact match validation
State validation Cryptographic random state