This guide provides complete, production-ready implementation snippets for setting up standard OpenID Connect (OIDC) authentication. Zero IDS exposes standard OIDC discovery endpoints, allowing compatibility with standard client libraries.
We use the official, certified openid-client package alongside express-session to manage state, tokens, and verification:
npm install openid-client express express-session
Initialize OIDC using discovery. The discovery endpoint fetches OIDC metadata dynamically (authorization endpoint, token endpoint, JWKS URI, scopes supported):
const { Issuer, generators } = require('openid-client');
const express = require('express');
const session = require('express-session');
const app = express();
// Configure session storage. Use memory store only for development.
// In production, configure RedisStore or MongoStore for persistence and scaling.
app.use(session({
secret: 'a-secure-random-session-secret-key-12345',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Prevents XSS attacks from reading session ID
secure: false, // Set to true in production with HTTPS
sameSite: 'lax' // Protects against CSRF attacks
}
}));
// Global client container
let client;
// Discover OIDC server parameters dynamically
async function initOidc() {
try {
const issuer = await Issuer.discover('https://ids.fabrixly.com/oidc');
console.log('Discovered issuer metadata:', issuer.metadata.issuer);
client = new issuer.Client({
client_id: 'your-client-id',
client_secret: 'your-client-secret',
redirect_uris: ['http://localhost:3001/callback'],
response_types: ['code']
});
} catch (err) {
console.error('OIDC Discovery failed:', err);
}
}
initOidc();
Issuer.discover method requests https://ids.fabrixly.com/oidc/.well-known/openid-configuration. This configuration document ensures your app automatically syncs configuration changes (like token signing key rotation).
Create routes for login redirects and exchange callbacks. Below is the code implementation followed by the actual HTTP request/response wire representations:
// 1. Initiate authorization redirect
app.get('/login', (req, res) => {
// Generate PKCE code verifier and code challenge
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
// Generate secure state to prevent Cross-Site Request Forgery (CSRF)
const state = generators.state();
const nonce = generators.nonce();
// Persist values in user session for validation upon callback redirect
req.session.code_verifier = code_verifier;
req.session.state = state;
req.session.nonce = nonce;
const authUrl = client.authorizationUrl({
scope: 'openid profile email offline_access', // Include offline_access to receive refresh tokens
state: state,
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: 'S256'
});
res.redirect(authUrl);
});
// 2. Process OIDC callback response redirect
app.get('/callback', async (req, res) => {
const params = client.callbackParams(req);
try {
// Exchange authorization code for TokenSet
const tokenSet = await client.callback(
'http://localhost:3001/callback',
params,
{
code_verifier: req.session.code_verifier,
state: req.session.state,
nonce: req.session.nonce
}
);
// Save the resulting tokens in the session
req.session.tokenSet = tokenSet;
// Retrieve profile details using the UserInfo endpoint
req.session.userInfo = await client.userinfo(tokenSet);
res.redirect('/profile');
} catch (err) {
console.error('Authorization Callback Exchange failure:', err);
res.status(500).send('Authentication failed: ' + err.message);
}
});
Redirection to Authorization endpoint:
GET /auth?response_type=code&client_id=web-portal&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&scope=openid%20profile%20email&state=abc123state&code_challenge=xyzChallenge&code_challenge_method=S256 HTTP/1.1 Host: localhost:3000
Redirect Response from Authorization endpoint:
HTTP/1.1 302 Found Location: http://localhost:3001/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=abc123state
Back-channel Token Exchange request:
POST /token HTTP/1.1 Host: localhost:3000 Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&client_id=web-portal&client_secret=secret123&code_verifier=highEntropyVerifier
Token Response Payload:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "def50200b1...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}
Protect server-side endpoints with middleware. The middleware validates token state and handles automatic refreshing:
// Middleware asserting active token presence
async function requireAuth(req, res, next) {
if (!req.session.tokenSet) {
return res.redirect('/login');
}
// Check if token has expired or is close to expiration
let tokenSet = new client.issuer.TokenSet(req.session.tokenSet);
if (tokenSet.expired()) {
try {
console.log('Token expired. Attempting refresh token grant...');
const refreshed = await client.refresh(tokenSet.refresh_token);
req.session.tokenSet = refreshed;
tokenSet = refreshed;
} catch (err) {
console.error('Refresh Token validation failed. Requesting re-authorization:', err);
return res.redirect('/login');
}
}
req.tokenSet = tokenSet;
next();
}
// Protected Resource Endpoint
app.get('/profile', requireAuth, (req, res) => {
res.json({
message: 'Resource accessed successfully',
user: req.session.userInfo,
claims: req.tokenSet.claims() // Parsed ID token claims
});
});
// Logout Handler
app.get('/logout', async (req, res) => {
const tokenSet = req.session.tokenSet;
req.session.destroy(() => {
if (tokenSet && tokenSet.id_token) {
// Initiate OIDC RP-Initiated logout to clear SSO session on Identity Server
const logoutUrl = client.endSessionUrl({
id_token_hint: tokenSet.id_token,
post_logout_redirect_uri: 'http://localhost:3001/'
});
return res.redirect(logoutUrl);
}
res.redirect('/');
});
});
app.listen(3001, () => {
console.log('Service operational at http://localhost:3001');
console.log('Initiate flow at http://localhost:3001/login');
});