This document details integration scenarios across standard grant types, providing full, runnable Node.js and Express implementations for developers to build production integrations.
Use Case: Most secure flow for Web Applications. Uses dynamic Proof Key for Code Exchange (PKCE) keys to protect tokens from interception.
const { Issuer, generators } = require('openid-client');
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'saas-session-secret',
resave: false,
saveUninitialized: false
}));
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({
client_id: 'saas-confidential-client-id',
client_secret: 'saas-confidential-client-secret',
redirect_uris: ['http://localhost:3001/callback'],
response_types: ['code']
});
})();
app.get('/login', (req, res) => {
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
const state = generators.state();
req.session.code_verifier = code_verifier;
req.session.state = state;
const authUrl = client.authorizationUrl({
scope: 'openid profile email offline_access',
code_challenge: code_challenge,
code_challenge_method: 'S256',
state: state
});
res.redirect(authUrl);
});
app.get('/callback', async (req, res) => {
const params = client.callbackParams(req);
try {
const tokenSet = await client.callback('http://localhost:3001/callback', params, {
code_verifier: req.session.code_verifier,
state: req.session.state
});
req.session.tokenSet = tokenSet;
req.session.userInfo = await client.userinfo(tokenSet.access_token);
res.redirect('/profile');
} catch (err) {
res.status(400).send('Authorization validation failed: ' + err.message);
}
});
app.get('/profile', (req, res) => {
if (!req.session.tokenSet) return res.redirect('/login');
res.json({ userInfo: req.session.userInfo, tokens: req.session.tokenSet });
});
app.listen(3001, () => console.log('App running on http://localhost:3001'));
Use Case: Front-end apps receive ID tokens directly (for rendering client-side UI configurations immediately) while keeping access tokens restricted to back-channel exchange.
const { Issuer, generators } = require('openid-client');
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({ secret: 'hybrid-session-secret', resave: false, saveUninitialized: false }));
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({
client_id: 'hybrid-client-id',
client_secret: 'hybrid-client-secret',
redirect_uris: ['http://localhost:3001/callback'],
response_types: ['code id_token'] // Ask for authorization code and ID token
});
})();
app.get('/login', (req, res) => {
const nonce = generators.nonce();
const state = generators.state();
req.session.nonce = nonce;
req.session.state = state;
const authUrl = client.authorizationUrl({
scope: 'openid profile email',
response_type: 'code id_token',
response_mode: 'form_post', // Recommended mode to prevent token details leaking in URI logs
nonce: nonce,
state: state
});
res.redirect(authUrl);
});
// Since response_mode is form_post, handling callback as a POST request
app.post('/callback', async (req, res) => {
try {
const params = client.callbackParams(req);
const tokenSet = await client.callback('http://localhost:3001/callback', params, {
nonce: req.session.nonce,
state: req.session.state
});
req.session.tokenSet = tokenSet;
res.json({ message: 'Login successful via Hybrid Flow', claims: tokenSet.claims() });
} catch (err) {
res.status(500).send('Authentication Exchange failed: ' + err.message);
}
});
app.listen(3001, () => console.log('App running on http://localhost:3001'));
Use Case: Legacy browser apps. Directly returns tokens in the URL fragment.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Legacy Implicit Flow Client</title>
</head>
<body>
<h2>OIDC Implicit Flow Demo</h2>
<button id="loginBtn">Log In</button>
<div id="output"></div>
<script>
const clientId = 'legacy-implicit-spa-id';
const issuerUrl = 'http://localhost:3000/oidc';
const redirectUri = window.location.origin + window.location.pathname;
document.getElementById('loginBtn').onclick = () => {
const state = Math.random().toString(36).substring(2);
const nonce = Math.random().toString(36).substring(2);
localStorage.setItem('state', state);
localStorage.setItem('nonce', nonce);
const authUrl = `${issuerUrl}/auth` +
`?response_type=id_token token` +
`&client_id=${encodeURIComponent(clientId)}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&scope=openid profile email` +
`&response_mode=fragment` +
`&state=${state}` +
`&nonce=${nonce}`;
window.location.href = authUrl;
};
// Parse fragment tokens upon redirect callback
if (window.location.hash) {
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const idToken = params.get('id_token');
const returnedState = params.get('state');
// Validate state parameter to mitigate CSRF
const savedState = localStorage.getItem('state');
if (returnedState === savedState) {
document.getElementById('output').innerText = `Login success!\nAccess Token: ${accessToken}\nID Token: ${idToken}`;
window.location.hash = ''; // Clear tokens from address bar
} else {
document.getElementById('output').innerText = 'State mismatch. Possible CSRF attack detected!';
}
}
</script>
</body>
</html>
Use Case: Secure service-to-service Machine-to-Machine communication without user presence context.
const { Issuer } = require('openid-client');
const axios = require('axios');
async function performM2MCall() {
try {
// 1. Discover the Provider
const issuer = await Issuer.discover('http://localhost:3000/oidc');
// 2. Initialize Client with secret authentication methods
const client = new issuer.Client({
client_id: 'm2m-client-id',
client_secret: 'm2m-client-secret-hex-key',
token_endpoint_auth_method: 'client_secret_post'
});
// 3. Execute client credentials grant request
console.log('Requesting access token...');
const tokenSet = await client.grant({
grant_type: 'client_credentials',
scope: 'api:read billing:write' // Request scopes configured for this client
});
console.log('M2M Access Token acquired successfully:', tokenSet.access_token);
// 4. Request protected API endpoints
const response = await axios.get('http://localhost:3002/protected-api', {
headers: {
Authorization: `Bearer ${tokenSet.access_token}`
}
});
console.log('API Resource response data:', response.data);
} catch (err) {
console.error('M2M Integration Failed:', err.message);
}
}
performM2MCall();
Use Case: Maintain uninterrupted sessions without forcing the user to log in again.
const { Issuer } = require('openid-client');
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({ secret: 'refresh-session-secret', resave: false, saveUninitialized: false }));
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({
client_id: 'web-portal',
client_secret: 'secret123'
});
})();
// Middleware that automatically renews expired access tokens using the refresh token
async function checkAndRefreshTokens(req, res, next) {
if (!req.session.tokenSet) {
return res.status(401).send('Session not authenticated.');
}
// Restore TokenSet instance properties
let tokenSet = new client.issuer.TokenSet(req.session.tokenSet);
if (tokenSet.expired()) {
console.log('Access Token expired. Requesting refresh grant...');
try {
const refreshed = await client.refresh(tokenSet.refresh_token);
req.session.tokenSet = refreshed;
console.log('Tokens refreshed successfully.');
} catch (err) {
console.error('Failed to refresh tokens. Redirecting to login:', err.message);
return res.redirect('/login');
}
}
next();
}
app.get('/api-data', checkAndRefreshTokens, (req, res) => {
res.json({
message: 'Authorized successfully',
accessToken: req.session.tokenSet.access_token
});
});
app.listen(3001);
Use Case: Resource Servers (APIs) query the Identity Server directly to validate access tokens.
const express = require('express');
const { Issuer } = require('openid-client');
const app = express();
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({
client_id: 'api-resource-server-id',
client_secret: 'api-resource-server-secret-key'
});
})();
// Token Introspection Middleware
async function introspectToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing Bearer token.' });
}
const token = authHeader.split(' ')[1];
try {
const introspection = await client.introspect(token, 'access_token');
if (!introspection.active) {
return res.status(401).json({ error: 'Token is expired or has been revoked.' });
}
// Attach token context claims to request objects
req.tokenContext = introspection;
next();
} catch (err) {
return res.status(500).json({ error: 'Introspection request failure: ' + err.message });
}
}
app.get('/protected-data', introspectToken, (req, res) => {
res.json({
message: 'Protected resource accessed!',
scopes: req.tokenContext.scope,
clientId: req.tokenContext.client_id,
userSubject: req.tokenContext.sub
});
});
app.listen(3002, () => console.log('API Resource Server operational at port 3002'));
Use Case: Explicitly revoke tokens upon user logout to terminate access instantly.
const { Issuer } = require('openid-client');
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({ secret: 'revoke-session-secret', resave: false, saveUninitialized: false }));
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({ client_id: 'web-portal', client_secret: 'secret123' });
})();
app.get('/logout-revoke', async (req, res) => {
if (req.session.tokenSet) {
try {
const tokenSet = req.session.tokenSet;
// Revoke access token
await client.revoke(tokenSet.access_token, 'access_token');
// Revoke refresh token if present
if (tokenSet.refresh_token) {
await client.revoke(tokenSet.refresh_token, 'refresh_token');
}
console.log('Tokens successfully revoked.');
} catch (err) {
console.error('Revocation endpoints returned error:', err.message);
}
}
req.session.destroy(() => {
res.send('Logged out and tokens revoked successfully.');
});
});
app.listen(3001);
Use Case: Query profile details (e.g. firstName, email, custom claims) using an active access token.
const { Issuer } = require('openid-client');
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({ secret: 'userinfo-session-secret', resave: false, saveUninitialized: false }));
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({ client_id: 'web-portal', client_secret: 'secret123' });
})();
app.get('/fetch-profile', async (req, res) => {
if (!req.session.tokenSet) {
return res.status(401).send('Not authenticated.');
}
try {
const accessToken = req.session.tokenSet.access_token;
// Call OIDC UserInfo endpoint
const userInfo = await client.userinfo(accessToken);
res.json({
message: 'UserInfo retrieved successfully',
profile: userInfo
});
} catch (err) {
res.status(500).send('UserInfo request failed: ' + err.message);
}
});
app.listen(3001);
Use Case: Standardized flow to terminate single-sign-on (SSO) sessions on the identity server.
const { Issuer } = require('openid-client');
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({ secret: 'logout-session-secret', resave: false, saveUninitialized: false }));
let client;
(async () => {
const issuer = await Issuer.discover('http://localhost:3000/oidc');
client = new issuer.Client({
client_id: 'web-portal',
client_secret: 'secret123',
post_logout_redirect_uris: ['http://localhost:3001/logout-success']
});
})();
app.get('/logout', (req, res) => {
const tokenSet = req.session.tokenSet;
if (!tokenSet) return res.redirect('/');
// Generate URL for redirecting user to terminate the SSO session
const logoutUrl = client.endSessionUrl({
id_token_hint: tokenSet.id_token, // Recommended to identify session
post_logout_redirect_uri: 'http://localhost:3001/logout-success'
});
// Clear local session state first
req.session.destroy(() => {
// Redirect to OIDC provider session termination endpoint
res.redirect(logoutUrl);
});
});
app.get('/logout-success', (req, res) => {
res.send('OIDC Session terminated successfully.');
});
app.listen(3001);