Avnology ID
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 express

Client 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

On this page