Node.js (Express) quickstart
🔑 Auth:
client_id+client_secret(server-side) · 👤 This IS the login — standard OpenID Connect · 🟩 Official SDK · ⏱ ~5 minutes
What you’ll build: a single-file Express app where “Connect with Valyd” returns the user’s
pseudonymous valyd_id and verification proofs to your backend.
Your real values are pre-filled on your app’s Quick setup tab in the Developer Portal .
Prefer to clone instead of typing? valyd-sandbox-starter
on GitHub (or download the zip) is this exact app — fill
.env, npm run dev. It also has an optional Test a verification workflow button: set
VALYD_VERIFY_API_KEY and VALYD_WORKFLOW_ID in .env to run a Verify workflow against the
signed-in account after login.
1. Create the app
In the Developer Portal create an application, enable the profile
scope, and register this exact redirect URI:
http://localhost:8080/callbackCopy the client_id and the one-time client_secret. Keep the secret on your server.
2. Install
mkdir valyd-login && cd valyd-login
npm init -y
npm install @valyd/sdk express express-session dotenv.env
VALYD_CLIENT_ID=YOUR_CLIENT_ID
VALYD_CLIENT_SECRET=YOUR_CLIENT_SECRET
VALYD_REDIRECT_URI=http://localhost:8080/callback
VALYD_IDP_URL=https://idp.valyd.work
SESSION_SECRET=replace_with_at_least_32_random_characters3. Write the server
server.mjs
import "dotenv/config";
import express from "express";
import session from "express-session";
import { Valyd } from "@valyd/sdk";
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: "lax", secure: false, maxAge: 10 * 60 * 1000 },
}));
const valyd = new Valyd({
clientId: process.env.VALYD_CLIENT_ID,
clientSecret: process.env.VALYD_CLIENT_SECRET,
redirectUri: process.env.VALYD_REDIRECT_URI,
idpBaseUrl: process.env.VALYD_IDP_URL,
});
app.get("/", (_req, res) => res.type("html").send('<a href="/login">Connect with Valyd</a>'));
app.get("/login", (req, res, next) => {
try {
const transaction = valyd.auth.createAuthorizationRequest({ scope: ["profile"] });
req.session.valydOidc = transaction; // server-side: state, nonce, and PKCE verifier
req.session.save((error) => error ? next(error) : res.redirect(transaction.url));
} catch (error) {
next(error);
}
});
app.get("/callback", async (req, res, next) => {
try {
const transaction = req.session.valydOidc;
if (!transaction) return res.status(400).send("Login transaction missing or expired");
delete req.session.valydOidc;
await new Promise((resolve, reject) => req.session.save((error) => error ? reject(error) : resolve()));
const callbackUrl = new URL(req.originalUrl, process.env.VALYD_REDIRECT_URI).toString();
const { user, tokens } = await valyd.auth.handleCallback(callbackUrl, { transaction });
// Create your own app session here. Do not send Valyd tokens to browser storage.
res.json({ valydId: user.valyd_id, idVerified: user.id_verified, scopes: tokens.scope });
} catch (error) {
next(error);
}
});
app.use((error, _req, res, _next) => {
console.error(error.code ?? error.name, error.message);
res.status(400).json({ error: error.code ?? "login_failed", message: error.message });
});
app.listen(8080, () => console.log("Open http://localhost:8080"));4. Run it
node server.mjsOpen http://localhost:8080 and click Connect with Valyd. A successful callback returns the
user’s pseudonymous valydId, verification proof, and granted scopes.
Checkpoint: GET /login returns a 302 to /api/auth/oidc/authorize whose URL contains
state, nonce, code_challenge, and code_challenge_method=S256; the callback succeeds only
with the stored server-side transaction; handleCallback() verifies state, PKCE, RS256/JWKS,
issuer, audience, expiry, and nonce; and the client_secret, PKCE verifier, and tokens never
enter browser JavaScript or local storage.
For production, use a shared session store, set the cookie to secure: true, configure trusted
proxy handling correctly, and use your HTTPS callback URI.
Troubleshooting
redirect_urimismatch — the URI must match a registered redirect URI exactly (scheme, host, path). See Errors & troubleshooting.- “Login transaction missing or expired” — the session cookie didn’t survive the round-trip;
keep
sameSite: "lax"and start login and callback on the same host/port. See Errors & troubleshooting. invalid_granton the exchange — the code is single-use and short-lived; a double-fired callback or a slow retry consumes it. Restart from/login. See Errors & troubleshooting.
Next steps
- Scopes — request
verifications,email, or license scopes. - Account API — read userinfo, licenses, and verification proofs with the access token.
- Attach a verification — run a new KYC or license check with the user’s token so the proof saves to their Valyd account.