SDKsTypeScript SDKGuides
Express.js Integration
Protect Express.js APIs with Avnology ID token validation, permission checks, and webhook handling.
Express.js Integration
This guide shows how to protect Express.js APIs using the TypeScript SDK for token validation, permission checking, and webhook processing.
Setup
npm install @avnology/sdk-typescript expressClient initialization
// lib/avnology.ts
import { AvnologyId } from "@avnology/sdk-typescript";
export const avnology = new AvnologyId({
baseUrl: process.env.AVNOLOGY_BASE_URL!,
clientId: process.env.AVNOLOGY_CLIENT_ID!,
clientSecret: process.env.AVNOLOGY_CLIENT_SECRET!,
});Authentication middleware
Validate Bearer tokens on incoming requests.
// middleware/auth.ts
import { avnology } from "../lib/avnology";
import { AvnologyIdError } from "@avnology/sdk-typescript";
import type { Request, Response, NextFunction } from "express";
declare global {
namespace Express {
interface Request {
userId?: string;
scopes?: string[];
orgId?: string;
}
}
}
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({
error: "missing_token",
message: "Authorization header with Bearer token is required.",
});
}
try {
const token = authHeader.slice(7);
const result = await avnology.oauth.introspectToken({ token });
if (!result.active) {
return res.status(401).json({
error: "invalid_token",
message: "The access token is expired or revoked.",
});
}
req.userId = result.sub;
req.scopes = result.scope.split(" ");
req.orgId = result.orgId;
next();
} catch (error) {
if (error instanceof AvnologyIdError) {
return res.status(500).json({
error: "auth_service_error",
message: "Unable to validate token. Please try again.",
});
}
throw error;
}
}Scope requirement middleware
export function requireScope(...requiredScopes: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userScopes = req.scopes ?? [];
const missing = requiredScopes.filter((s) => !userScopes.includes(s));
if (missing.length > 0) {
return res.status(403).json({
error: "insufficient_scope",
message: `Missing required scopes: ${missing.join(", ")}`,
});
}
next();
};
}Using the middleware
import express from "express";
import { requireAuth, requireScope } from "./middleware/auth";
const app = express();
// Public routes
app.get("/health", (req, res) => res.json({ status: "ok" }));
// Protected routes
app.get("/api/profile", requireAuth, async (req, res) => {
const user = await avnology.admin.getUser({ userId: req.userId! });
res.json(user);
});
// Scope-protected routes
app.get("/api/users", requireAuth, requireScope("users:read"), async (req, res) => {
const users = await avnology.admin.listUsers({ pageSize: 25 });
res.json(users);
});Permission middleware
// middleware/permissions.ts
import { avnology } from "../lib/avnology";
export function requirePermission(relation: string, getObject: (req: Request) => string) {
return async (req: Request, res: Response, next: NextFunction) => {
const allowed = await avnology.permissions.check({
subject: `user:${req.userId}`,
relation,
object: getObject(req),
});
if (!allowed) {
return res.status(403).json({
error: "forbidden",
message: "You do not have permission to access this resource.",
});
}
next();
};
}
// Usage:
app.put(
"/api/projects/:id",
requireAuth,
requirePermission("editor", (req) => `project:${req.params.id}`),
async (req, res) => {
// User is verified as editor of this project
res.json({ updated: true });
}
);Webhook handler
import { verifyWebhookSignature } from "@avnology/sdk-typescript";
app.post("/webhooks/avnology",
express.raw({ type: "application/json" }),
(req, res) => {
const isValid = verifyWebhookSignature({
payload: req.body,
signature: req.headers["x-avnology-signature"] as string,
secret: process.env.WEBHOOK_SECRET!,
});
if (!isValid) {
return res.status(401).json({ error: "invalid_signature" });
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case "user.created":
handleUserCreated(event.data);
break;
case "user.deleted":
handleUserDeleted(event.data);
break;
default:
console.log("Unhandled event:", event.type);
}
res.status(200).json({ received: true });
}
);See also
- React guide -- Frontend integration
- Testing guide -- Testing with mocks
- Webhook verification -- Signature verification