HonkIO Auth
Démarrage rapide Express
HonkIO Auth dans une application Express avec openid-client et express-session, en un seul fichier.
Installation
Installez Express, express-session et openid-client. Créez une application de test avec l’URI de redirection http://localhost:3000/auth/callback et l’URI de redirection après déconnexion http://localhost:3000/, et définissez HONKIO_AUTH_ISSUER, HONKIO_AUTH_CLIENT_ID, HONKIO_AUTH_CLIENT_SECRET, APP_URL et SESSION_SECRET.
npm install express express-session openid-clientL’application
Toute l’application, en un fichier. La connexion envoie PKCE, state et nonce; le rappel les vérifie, ainsi que le iss et le aud du jeton d’identité, puis ouvre une nouvelle session.
// server.js (ES module)
import express from 'express'
import session from 'express-session'
import * as client from 'openid-client'
const ISSUER = process.env.HONKIO_AUTH_ISSUER // https://login-dev.acme.ca
const APP_URL = process.env.APP_URL // http://localhost:3000
const OUTAGE_GRACE_MS = 24 * 60 * 60 * 1000
const config = await client.discovery(new URL(ISSUER), process.env.HONKIO_AUTH_CLIENT_ID, process.env.HONKIO_AUTH_CLIENT_SECRET)
config.timeout = 5
const app = express()
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production' },
// Use a persistent store (Redis, Postgres) in production.
}))
function saveTokens(req, tokens) {
const claims = tokens.claims()
if (claims) req.session.user = { sub: claims.sub, email: claims.email }
if (tokens.id_token) req.session.idToken = tokens.id_token
if (tokens.refresh_token) req.session.refreshToken = tokens.refresh_token
req.session.accessToken = tokens.access_token
req.session.accessExpiresAt = Date.now() + (tokens.expiresIn() ?? 600) * 1000
delete req.session.retryRefreshAt
}
app.get('/auth/login', async (req, res) => {
const verifier = client.randomPKCECodeVerifier()
const state = client.randomState()
const nonce = client.randomNonce()
req.session.pending = { verifier, state, nonce }
const url = client.buildAuthorizationUrl(config, {
redirect_uri: APP_URL + '/auth/callback',
scope: 'openid email',
code_challenge: await client.calculatePKCECodeChallenge(verifier),
code_challenge_method: 'S256',
state,
nonce,
})
res.redirect(url.href)
})
app.get('/auth/callback', async (req, res, next) => {
try {
const pending = req.session.pending
if (!pending) return res.redirect('/auth/login')
const tokens = await client.authorizationCodeGrant(config, new URL(req.originalUrl, APP_URL), {
pkceCodeVerifier: pending.verifier,
expectedState: pending.state,
expectedNonce: pending.nonce,
idTokenExpected: true,
})
delete req.session.pending
req.session.regenerate((err) => {
if (err) return next(err)
saveTokens(req, tokens)
res.redirect('/')
})
} catch (err) {
next(err)
}
})
/** True only for "HonkIO can't be reached": a network error, a timeout or a 5xx. */
function isUnreachable(err) {
if (err instanceof client.ResponseBodyError) return err.status >= 500
if (err?.name === 'TimeoutError' || err?.name === 'AbortError') return true
if (err instanceof TypeError) return true
return err?.cause instanceof Response && err.cause.status >= 500
}
// Signed-in pages: refresh when the access token has expired, with the outage rule.
async function requireUser(req, res, next) {
const s = req.session
if (!s.user) return res.redirect('/auth/login')
const now = Date.now()
if (now < s.accessExpiresAt - 30_000) return next()
if (s.retryRefreshAt && now < s.retryRefreshAt && now < s.accessExpiresAt + OUTAGE_GRACE_MS) return next()
try {
saveTokens(req, await client.refreshTokenGrant(config, s.refreshToken))
return next()
} catch (err) {
if (isUnreachable(err) && now < s.accessExpiresAt + OUTAGE_GRACE_MS) {
// HonkIO can't be reached: keep the session, retry in a minute.
s.retryRefreshAt = now + 60_000
return next()
}
// invalid_grant, or the outage outlasted 24 hours past expiry: end the session now.
return s.destroy(() => res.redirect('/auth/login'))
}
}
app.get('/', requireUser, (req, res) => res.send('Signed in as ' + (req.session.user.email ?? req.session.user.sub)))
app.post('/auth/logout', (req, res) => {
const idToken = req.session.idToken
req.session.destroy(() => {
const url = client.buildEndSessionUrl(config, { post_logout_redirect_uri: APP_URL + '/', ...(idToken ? { id_token_hint: idToken } : {}) })
res.redirect(303, url.href)
})
})
app.listen(3000)Renouvellement et règle de panne
Le middleware requireUser renouvelle les jetons expirés. Si HonkIO est injoignable (erreur réseau, délai dépassé ou erreur 5xx), il garde la session jusqu’à 24 heures après l’expiration du jeton d’accès et réessaie chaque minute. Si HonkIO refuse le renouvellement (invalid_grant), il met fin à la session immédiatement. Voir Sessions et pannes.
HonkIO