UniAuth.ID

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-client

Configure 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