Sign-in with Human Pass in an Express app, with openid-client and express-session.
Setup
shell
npm install express express-session openid-client.env
HP_ISSUER=https://id.sandbox.humanpass.co
HP_CLIENT_ID=hp_test_…
HP_CLIENT_SECRET=hps_…
HP_REDIRECT_URI=http://localhost:3000/auth/callback
SESSION_SECRET=a-long-random-stringThe app
server.js
import express from "express";
import session from "express-session";
import * as oidc from "openid-client";
const config = await oidc.discovery(
new URL(process.env.HP_ISSUER),
process.env.HP_CLIENT_ID,
{ id_token_signed_response_alg: "ES256" },
oidc.ClientSecretBasic(process.env.HP_CLIENT_SECRET),
);
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" },
}));
app.get("/auth/login", async (req, res) => {
const flow = { verifier: oidc.randomPKCECodeVerifier(), state: oidc.randomState(), nonce: oidc.randomNonce() };
req.session.hpFlow = flow;
const url = oidc.buildAuthorizationUrl(config, {
redirect_uri: process.env.HP_REDIRECT_URI,
scope: "openid personhood",
code_challenge: await oidc.calculatePKCECodeChallenge(flow.verifier),
code_challenge_method: "S256",
state: flow.state,
nonce: flow.nonce,
});
res.redirect(url.href);
});
app.get("/auth/callback", async (req, res, next) => {
try {
const flow = req.session.hpFlow;
delete req.session.hpFlow;
// Cancelled, or couldn't be verified: Human Pass doesn't say which.
if (!flow || req.query.error) return res.redirect("/?signin=failed");
const callbackUrl = new URL(req.originalUrl, process.env.HP_REDIRECT_URI);
const tokens = await oidc.authorizationCodeGrant(config, callbackUrl, {
pkceCodeVerifier: flow.verifier,
expectedState: flow.state,
expectedNonce: flow.nonce,
idTokenExpected: true,
});
const claims = tokens.claims();
if (claims.verified_human !== true) return res.redirect("/?signin=failed");
const account = await accountFor(claims.sub, claims.previous_sub); // see the quickstart
req.session.regenerate((err) => {
if (err) return next(err);
req.session.accountId = account.id;
res.redirect("/");
});
} catch (err) {
next(err);
}
});
app.get("/", (req, res) => {
res.send(req.session.accountId
? "Signed in."
: '<a href="/auth/login">Continue with Human Pass</a>');
});
app.listen(3000);Notes
accountForis the lookup from the quickstart: key accounts onsub, and move an account whenprevious_subis present.req.session.regenerategives the signed-in person a new session ID, so a session started before sign-in can't be hijacked.- Schedule a daily status check for the IDs you hold.
- In production, use a persistent session store (Redis, your database), not the default memory store, and set
app.set("trust proxy", 1)behind a load balancer.