Human Pass DocsSandbox
DashboardCreate account
Menu

Express

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

The 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

  • accountFor is the lookup from the quickstart: key accounts on sub, and move an account when previous_sub is present.
  • req.session.regenerate gives 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.

The Human Pass sandbox is live for demos and testing integrations; production is coming soon. Interfaces may still change; we'll give notice of anything that breaks an integration.

humanpass.co · Security overview · Privacy