Node.js and Express
Sign users in to an Express app with the openid-client library: discovery, PKCE, the callback and a session.
This guide uses openid-client v6, a certified OpenID Connect library for Node.js.
Install
npm install express express-session openid-client
The app
import express from "express"
import session from "express-session"
import * as client from "openid-client"
const ISSUER = new URL("https://acme.gatekeepauth.com")
const CLIENT_ID = "acme-notes"
const REDIRECT_URI = "http://localhost:3000/auth/callback"
// Discovery reads /.well-known/openid-configuration. None() = public client, no secret.
const config = await client.discovery(ISSUER, CLIENT_ID, undefined, client.None())
const app = express()
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }))
app.get("/login", async (req, res) => {
const code_verifier = client.randomPKCECodeVerifier()
const state = client.randomState()
const nonce = client.randomNonce()
Object.assign(req.session, { code_verifier, state, nonce })
const url = client.buildAuthorizationUrl(config, {
redirect_uri: REDIRECT_URI,
scope: "openid email",
code_challenge: await client.calculatePKCECodeChallenge(code_verifier),
code_challenge_method: "S256",
state,
nonce,
})
res.redirect(url.href)
})
app.get("/auth/callback", async (req, res) => {
const { code_verifier, state, nonce } = req.session
const tokens = await client.authorizationCodeGrant(
config,
new URL(req.originalUrl, "http://localhost:3000"),
{ pkceCodeVerifier: code_verifier, expectedState: state, expectedNonce: nonce },
)
const claims = tokens.claims() // verified ID token claims
req.session.user = { id: claims.sub, email: claims.email }
res.redirect("/")
})
app.get("/", (req, res) => {
res.send(req.session.user ? `Hello ${req.session.user.email}` : `<a href="/login">Sign in</a>`)
})
app.listen(3000)
Register http://localhost:3000/auth/callback (and your production URL) as redirect URIs.
authorizationCodeGrant checks the state, the PKCE verifier, the ID token’s signature against your JWKS, its issuer, audience, expiry and nonce. If any check fails it throws, and nobody is signed in.