Verified Ham with Node.js (openid-client)
The full sign-in flow with the openid-client library, in an Express app.
openid-client is the standard OpenID Connect
library for Node.js. This is a complete Express app: sign in, then show the user's call sign.
It's also in the repository as examples/node.
1. Register your app
- Create a Verified Ham account, if you don't have one, and sign in.
- Go to Register an app.
- Give it a name. People see it when they sign in.
- Set the redirect URI to:
http://localhost:3000/callback - Choose Confidential, and register.
- Copy the client ID and client secret. The secret is shown only once.
You can sign in to your own app straight away, to test. Other people can sign in once you've verified your own call sign.
2. The app
npm install express express-session openid-client, then save this as
server.js (with "type": "module" in package.json) and run it
with VH_CLIENT_ID=… VH_CLIENT_SECRET=… node server.js.
// Sign in with Verified Ham in an Express app, with openid-client.
//
// npm install
// VH_CLIENT_ID=... VH_CLIENT_SECRET=... npm start
//
// Register the app at https://verifiedham.net/developers/apps/new with the
// redirect URI http://localhost:3000/callback, then open http://localhost:3000.
import express from 'express'
import session from 'express-session'
import * as client from 'openid-client'
const issuer = process.env.VH_ISSUER ?? 'https://verifiedham.net'
const redirectUri = 'http://localhost:3000/callback'
// Reads the endpoints from the discovery document.
const config = await client.discovery(new URL(issuer), process.env.VH_CLIENT_ID, process.env.VH_CLIENT_SECRET)
const app = express()
app.use(session({ secret: process.env.SESSION_SECRET ?? 'change me', resave: false, saveUninitialized: false }))
app.get('/login', async (req, res) => {
// PKCE: a fresh verifier per sign-in; only its hash goes in the link.
req.session.verifier = client.randomPKCECodeVerifier()
req.session.state = client.randomState()
res.redirect(client.buildAuthorizationUrl(config, {
redirect_uri: redirectUri,
scope: 'openid callsign',
state: req.session.state,
code_challenge: await client.calculatePKCECodeChallenge(req.session.verifier),
code_challenge_method: 'S256',
}).href)
})
app.get('/callback', async (req, res) => {
const tokens = await client.authorizationCodeGrant(config, new URL(req.originalUrl, redirectUri), {
pkceCodeVerifier: req.session.verifier,
expectedState: req.session.state,
})
// The ID token carries the claims for the scopes the user approved.
req.session.user = tokens.claims()
delete req.session.verifier
delete req.session.state
res.redirect('/')
})
app.get('/logout', (req, res) => req.session.destroy(() => res.redirect('/')))
app.get('/', (req, res) => {
const user = req.session.user
if (!user) return res.send('<a href="/login">Sign in with Verified Ham</a>')
// Key your users by sub, and trust the call sign only when callsign_verified is true.
const who = user.callsign_verified ? `<strong>${user.callsign}</strong>, verified` : 'without a verified call sign yet'
res.send(`<p>Signed in ${who}. <a href="/logout">Sign out</a></p><pre>${JSON.stringify(user, null, 2)}</pre>`)
})
app.listen(3000, () => console.log('http://localhost:3000'))
How it works
discovery()reads the endpoints fromhttps://verifiedham.net/.well-known/openid-configuration.- Each sign-in gets a fresh PKCE verifier and state, kept in the session.
authorizationCodeGrant()checks the state, sends the verifier and the client secret, and validates the ID token.tokens.claims()holdssub,callsignandcallsign_verified.