Guides Examples
Node.js — Express + openid-client
import { Issuer, generators } from 'openid-client';
const agewallet = await Issuer.discover('https://app.agewallet.io');
const client = new agewallet.Client({
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
redirect_uris: 'https://yourapp.com/callback',
response_types: ['code'],
});
// Generate PKCE
// Store this verifier in the user's session
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
app.get('/login', (req, res) => {
const authUrl = client.authorizationUrl({
scope: 'openid age',
state: 'xyz123', // Generate and store a random state in session
code_challenge,
code_challenge_method: 'S256',
nonce: generators.nonce(),
});
res.redirect(authUrl);
});
app.get('/callback', async (req, res, next) => {
try {
const params = client.callbackParams(req);
// Retrieve verifier and state from session
const tokenSet = await client.callback('https://yourapp.com/callback', params, {
code_verifier, // Retrieve from session
state: 'xyz123', // Retrieve from session
});
// Now use tokenSet.access_token to call the /userinfo endpoint
res.json(tokenSet);
} catch (err) {
next(err);
}
});// AgeWallet — Express + openid-client
//
// Targets openid-client@^5. Version 6 is a complete rewrite with a different
// API and this code will not run on it.
//
// Uses ESM: set "type": "module" in package.json.
import express from 'express';
import session from 'express-session';
import { Issuer, generators } from 'openid-client';
const app = express();
app.use( session( {
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
// Set false for local development over http, or the cookie is never sent
// and the session is empty when the callback runs.
secure: true
}
} ) );
const issuer = await Issuer.discover( 'https://app.agewallet.io' );
const client = new issuer.Client( {
client_id: process.env.AGEWALLET_CLIENT_ID,
client_secret: process.env.AGEWALLET_CLIENT_SECRET,
redirect_uris: [ 'https://yourapp.com/callback' ],
response_types: [ 'code' ]
} );
app.get( '/login', ( req, res ) => {
// One set of secrets per request, held on the session until the callback.
const codeVerifier = generators.codeVerifier();
const state = generators.state();
const nonce = generators.nonce();
req.session.agewallet = { codeVerifier, state, nonce };
res.redirect( client.authorizationUrl( {
scope: 'openid age',
state,
nonce,
code_challenge: generators.codeChallenge( codeVerifier ),
code_challenge_method: 'S256'
} ) );
} );
app.get( '/callback', async ( req, res, next ) => {
const pending = req.session.agewallet;
if ( ! pending ) {
return res.status( 400 ).send( 'No verification in progress.' );
}
// One attempt per authorization request.
delete req.session.agewallet;
try {
const tokenSet = await client.callback(
'https://yourapp.com/callback',
client.callbackParams( req ),
{
code_verifier: pending.codeVerifier,
state: pending.state,
nonce: pending.nonce
}
);
// client.callback() has validated the ID token signature, issuer,
// audience, expiry, state and nonce. That proves the user authenticated —
// it does not tell you their age status.
// The age result comes from UserInfo. Throws on a non-200, which lands in
// the catch below rather than being mistaken for an unverified user.
const userInfo = await client.userinfo( tokenSet );
// Store your own decision, not the tokens.
req.session.ageVerified = userInfo.age_verified === true;
res.redirect( '/' );
} catch ( err ) {
next( err );
}
} );