Express / Node
Verify Base IdP tokens in an Express or plain Node API — verify-only middleware, scope checks, and no required env.
An Express API behind a mobile or SPA frontend is verify-only. It receives a bearer token on each request, verifies it offline against published public keys, and serves the request. It does not run login. It does not call the token endpoint. It does not hold a secret.
This guide shows the full integration in Express, plus the equivalent in
plain Node http if you do not use Express.
Scaffold
npx base-idp create --stack expressThis writes middleware/baseIdp.ts, a .env (commented placeholder), and a
setup readme into ./base-idp-express/.
Install
npm install base-idpNo required env
# Verify-only. Nothing here is required.
# Optional: pin the audience.
# BASE_IDP_CLIENT_ID=sq_live_yourappA verify-only API can ship with no Base IdP env at all. Setting
BASE_IDP_CLIENT_ID is recommended for multi-tenant environments — it makes
the SDK reject tokens minted for a different app.
The middleware
import type { Request, Response, NextFunction } from "express";
import { BaseIdPServerClient } from "base-idp/server";
const client = new BaseIdPServerClient({
clientId: process.env.BASE_IDP_CLIENT_ID,
});
export async function requireAuth(
req: Request,
res: Response,
next: NextFunction,
) {
const header = req.headers.authorization ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
if (!token) {
return res.status(401).json({ error: "missing_token" });
}
try {
(req as any).principal = await client.verifyAccessToken(token);
next();
} catch (err) {
res.status(401).json({ error: "invalid_token" });
}
}The pattern is identical to what Passport-style middleware does, except
there is no strategy registration step — the SDK handles everything.
Using the middleware
import express from "express";
import { requireAuth } from "./middleware/baseIdp";
const app = express();
app.get("/me", requireAuth, (req, res) => {
res.json((req as any).principal);
});
app.listen(8080, () => console.log("listening on :8080"));For routes that should be public, omit the middleware. For routes that should be protected, add it.
A scope-checking middleware
The auth middleware proves the user is signed in. A separate middleware can enforce per-route permissions by checking scopes.
import type { Request, Response, NextFunction } from "express";
export function requireScope(...required: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const principal = (req as any).principal;
const granted: string[] = principal?.scopes ?? [];
const missing = required.filter((s) => !granted.includes(s));
if (missing.length) {
return res.status(403).json({
error: "insufficient_scope",
missing,
});
}
next();
};
}app.get(
"/admin/metrics",
requireAuth,
requireScope("admin", "metrics:read"),
handler,
);A typed request
If you use TypeScript, you can extend Express's Request type so
req.principal is properly typed.
import type { VerifiedPrincipal } from "base-idp/server";
declare global {
namespace Express {
interface Request {
principal?: VerifiedPrincipal;
}
}
}After this, drop the (req as any) casts in your handlers.
app.get("/me", requireAuth, (req, res) => {
res.json(req.principal);
});Plain Node http
If your service is not Express, the pattern is the same shape with a slightly different signature.
import http from "node:http";
import { BaseIdPServerClient } from "base-idp/server";
const client = new BaseIdPServerClient({
clientId: process.env.BASE_IDP_CLIENT_ID,
});
const server = http.createServer(async (req, res) => {
const header = req.headers.authorization ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
if (!token) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "missing_token" }));
return;
}
try {
const principal = await client.verifyAccessToken(token);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ id: principal.subject, email: principal.email }));
} catch {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "invalid_token" }));
}
});
server.listen(8080);A complete verify-only API
import express from "express";
import { requireAuth } from "./middleware/baseIdp";
import { requireScope } from "./middleware/requireScope";
const app = express();
app.use(express.json());
// Public routes
app.get("/healthz", (req, res) => res.json({ ok: true }));
// User-only routes
app.get("/me", requireAuth, (req, res) => {
res.json({
id: req.principal!.subject,
email: req.principal!.email,
scopes: req.principal!.scopes,
});
});
// Admin-only routes
app.get(
"/admin/users",
requireAuth,
requireScope("admin"),
async (req, res) => {
res.json(await db.user.findMany());
},
);
const port = process.env.PORT ?? 8080;
app.listen(port, () => console.log(`listening on :${port}`));Two middlewares, three routes, zero Base IdP env required. The whole
integration is the import and the requireAuth call.
Common issues
Cannot find module 'base-idp/server'
The SDK's server entry point is for Node only. If you are running this in
an edge runtime (Cloudflare Workers, Vercel Edge), use the browser entry
point's verifier instead.
Request hangs on first request after a deploy
The SDK fetches the public keys on first use. The first request waits for
that fetch. For warm-up, call client.publicKeys(false) at startup.
403 insufficient_scope for routes you expect to pass
The scopes on the token are not what you expect. Decode the token with the CLI to see what scopes it actually carries:
npx base-idp token v4.public.eyJ...