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-clientover 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
HttpOnlycookies.
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'));
});
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
Summary of Security Measures
- Authorization Code Flow + PKCE: Prevents authorization code injection and interception.
- No Tokens in Frontend: The frontend only sees a session cookie; the Express server handles the raw Access/Refresh tokens.
-
Dynamic Discovery: Using
Issuer.discoverensures your app stays compatible with provider updates (key rotation, endpoint changes).
Top comments (0)