DEV Community

Sameer Saleem
Sameer Saleem

Posted on

The Ultimate Guide to OAuth with Express.js (2025 Edition)

To build a secure, industry-standard authentication system in 2026, you must use the Backend for Frontend (BFF) pattern. This approach ensures that sensitive tokens (Access/Refresh tokens) are never exposed to the browser's JavaScript, mitigating XSS risks.

Core Principles

  • Use OpenID Connect (OIDC): Prefer openid-client over Passport.js for better OIDC compliance and PKCE support.
  • PKCE (Proof Key for Code Exchange): Always use PKCE, even for confidential server-side clients.
  • Secure Sessions: Use server-side sessions (e.g., Redis) and encrypted HttpOnly cookies.

The Codebase

Dependencies:
npm install express express-session openid-client dotenv

Implementation (app.js):

require('dotenv').config();
const express = require('express');
const session = require('express-session');
const { Issuer, generators } = require('openid-client');

const app = express();

// 1. Secure Session Configuration
app.use(session({
  name: '__Host-auth-session',
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

let client;

// 2. OIDC Client Discovery & Setup
async function initializeOidc() {
  const issuer = await Issuer.discover(process.env.OIDC_ISSUER);
  client = new issuer.Client({
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
    redirect_uris: [process.env.REDIRECT_URI],
    response_types: ['code']
  });
}

// 3. Login Route (Initiate PKCE)
app.get('/login', (req, res) => {
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);

  req.session.code_verifier = code_verifier;

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

  res.redirect(authUrl);
});

// 4. Callback Route (Exchange Code for Tokens)
app.get('/callback', async (req, res) => {
  try {
    const params = client.callbackParams(req);
    const tokenSet = await client.callback(process.env.REDIRECT_URI, params, {
      code_verifier: req.session.code_verifier,
    });

    // Store tokens in session, NOT in browser storage
    req.session.tokens = tokenSet;
    req.session.user = tokenSet.claims();

    res.redirect('/dashboard');
  } catch (err) {
    console.error('OAuth Callback Error:', err);
    res.status(500).send('Authentication failed');
  }
});

// 5. Protected Route Example
app.get('/dashboard', (req, res) => {
  if (!req.session.user) return res.status(401).send('Unauthorized');
  res.send(`Welcome, ${req.session.user.email}`);
});

initializeOidc().then(() => {
  app.listen(3000, () => console.log('BFF Server running on http://localhost:3000'));
});

Enter fullscreen mode Exit fullscreen mode

Environment Variables (.env)

OIDC_ISSUER=https://your-provider.com
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
REDIRECT_URI=http://localhost:3000/callback
SESSION_SECRET=a_very_long_random_string_here

Enter fullscreen mode Exit fullscreen mode

Summary of Security Measures

  1. Authorization Code Flow + PKCE: Prevents authorization code injection and interception.
  2. No Tokens in Frontend: The frontend only sees a session cookie; the Express server handles the raw Access/Refresh tokens.
  3. Dynamic Discovery: Using Issuer.discover ensures your app stays compatible with provider updates (key rotation, endpoint changes).

Top comments (0)