Express Quickstart
Use openid-client (the official OpenID Foundation certified library) with Express and express-session. UniAuth's discovery URL handles configuration automatically.
Install
npm install express express-session openid-clientConfigure OIDC Client
// auth.js
import { Issuer, generators } from 'openid-client'
let client
export async function getClient() {
if (client) return client
// Discovery — openid-client fetches everything from UniAuth
const issuer = await Issuer.discover('https://uniauth.id')
client = new issuer.Client({
client_id: process.env.UNIAUTH_CLIENT_ID,
client_secret: process.env.UNIAUTH_CLIENT_SECRET,
redirect_uris: ['https://yourapp.com/callback'],
post_logout_redirect_uris: ['https://yourapp.com/'],
response_types: ['code'],
token_endpoint_auth_method: 'client_secret_post',
})
return client
}Login + Callback Routes
// index.js
import express from 'express'
import session from 'express-session'
import { generators } from 'openid-client'
import { getClient } from './auth.js'
const app = express()
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: 'lax' },
}))
// GET /login — kick off the OIDC flow
app.get('/login', async (req, res) => {
const client = await getClient()
const codeVerifier = generators.codeVerifier()
const codeChallenge = generators.codeChallenge(codeVerifier)
const state = generators.state()
const nonce = generators.nonce()
req.session.codeVerifier = codeVerifier
req.session.state = state
req.session.nonce = nonce
const url = client.authorizationUrl({
scope: 'openid profile email groups',
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
})
res.redirect(url)
})
// GET /callback — verify ID token, create session
app.get('/callback', async (req, res) => {
const client = await getClient()
const params = client.callbackParams(req)
const tokenSet = await client.callback(
'https://yourapp.com/callback',
params,
{
code_verifier: req.session.codeVerifier,
state: req.session.state,
nonce: req.session.nonce,
}
)
// openid-client validates ID token: signature (via JWKS), iss, aud, exp, nonce
const claims = tokenSet.claims()
// Authorization checks using the verified claims
if (!claims.email_verified) {
return res.status(403).send('Email not verified')
}
const isAdmin = (claims.groups || []).includes('admin')
req.session.user = {
sub: claims.sub,
name: claims.name,
email: claims.email,
groups: claims.groups,
isAdmin,
}
req.session.idToken = tokenSet.id_token
res.redirect('/')
})
// GET /logout — propagate to UniAuth
app.get('/logout', async (req, res) => {
const client = await getClient()
const idToken = req.session.idToken
req.session.destroy(() => {
const url = client.endSessionUrl({
id_token_hint: idToken,
post_logout_redirect_uri: 'https://yourapp.com/',
})
res.redirect(url)
})
})
// Middleware example: require admin group
function requireAdmin(req, res, next) {
if (!req.session.user?.isAdmin) return res.status(403).end()
next()
}
app.get('/admin', requireAdmin, (req, res) => { res.send('admin area') })
app.listen(3000)Back-Channel Logout
// logout-webhook — register this URL in Developer Console
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://uniauth.id/.well-known/jwks.json'))
app.post('/api/logout-webhook', express.urlencoded(), async (req, res) => {
try {
const { payload } = await jwtVerify(req.body.logout_token, JWKS, {
issuer: 'https://uniauth.id',
audience: process.env.UNIAUTH_CLIENT_ID,
})
if (!payload.events?.['http://schemas.openid.net/event/backchannel-logout']) {
return res.status(400).end()
}
await invalidateSessions(payload.sub, payload.sid)
res.status(200).end()
} catch (err) {
res.status(400).end()
}
})Alternatives
- • Passport: passport-openidconnect — also works with UniAuth's discovery URL.
- • Fastify: fastify-oauth2 with manual ID token verification.