Wocha Docs

Express SDK

JWT validation, encrypted session cookies, SpiceDB permission checks, and webhook verification for Express APIs.

@wocha/express is the Express.js middleware for Wocha authentication and authorisation. It validates JWT access tokens against your issuer JWKS, resolves encrypted BFF session cookies, checks SpiceDB permissions via the Customer API, and verifies webhook signatures.

Installation

pnpm add @wocha/express
Or scaffold with the CLI
npx @wocha/cli init --framework express

Peer dependency: Express 4.18+ or 5.0+.

Environment variables

.env
WOCHA_ISSUER_URL=https://my-tenant.auth.wocha.ai
WOCHA_CLIENT_ID=your-client-id
WOCHA_CLIENT_SECRET=your-client-secret
WOCHA_AUDIENCE=your-api-audience          # optional
WOCHA_API_URL=https://my-tenant.api.wocha.ai  # optional — permission checks
WOCHA_COOKIE_SECRET=optional-cookie-secret       # optional — defaults to client secret
WOCHA_WEBHOOK_SECRET=whsec_...                 # webhook receiver only

WOCHA_ISSUER is accepted as an alias for WOCHA_ISSUER_URL.

Protect API routes

src/index.ts
import express from "express";
import { wochaAuth } from "@wocha/express";
 
const app = express();
app.use(express.json());
 
const auth = wochaAuth({
  issuerUrl: process.env.WOCHA_ISSUER_URL!,
  clientId: process.env.WOCHA_CLIENT_ID!,
  clientSecret: process.env.WOCHA_CLIENT_SECRET!,
  audience: process.env.WOCHA_AUDIENCE,
});
 
// Attach req.wocha on every request (user may be null)
app.use(auth.middleware());
 
app.get("/health", (_req, res) => {
  res.json({ status: "ok" });
});
 
app.get("/api/profile", auth.requireAuth(), (req, res) => {
  res.json(req.wocha.user);
});
 
app.delete("/api/posts/:id", auth.requirePermission("post", "delete"), (req, res) => {
  res.status(204).send();
});
 
app.get("/api/org-dashboard", auth.requireOrg(), (req, res) => {
  res.json({ org: req.wocha.org });
});

When env vars are set, you can omit the config object:

import { wochaAuth, wochaAuthConfigFromEnv } from "@wocha/express";
 
const auth = wochaAuth(wochaAuthConfigFromEnv()!);

JWT validation

Access tokens are validated with jose against your issuer JWKS. The middleware:

  1. Fetches OIDC discovery from {issuer}/.well-known/openid-configuration
  2. Caches JWKS keys for five minutes
  3. Verifies signature, issuer, expiry, and optional audience

Use validateAccessToken() directly when you need claims outside middleware:

import { validateAccessToken, WochaAuthError } from "@wocha/express";
 
try {
  const claims = await validateAccessToken(token, config);
} catch (err) {
  if (err instanceof WochaAuthError && err.code === "invalid_access_token") {
    // handle invalid token
  }
}

Call clearJwksCache() after key rotation if validation fails unexpectedly.

Session cookies

The middleware resolves authentication from two sources:

  1. Authorization: Bearer — validates the JWT directly
  2. Encrypted session cookie — compatible with @wocha/nextjs and @wocha/remix BFF cookies

When a cookie session is near expiry, tokens are refreshed automatically and a new Set-Cookie header is returned.

Permission middleware

requirePermission(resourceType, permission, options?) calls the Customer API /permissions/check endpoint using the authenticated user's access token:

app.patch(
  "/api/documents/:id",
  auth.requireAuth(),
  auth.requirePermission("document", "edit", {
    resourceId: (req) => req.params.id,
  }),
  updateDocumentHandler,
);

Returns 401 when unauthenticated, 403 when denied, and 400 when no resource ID can be resolved.

requireOrg(orgId?) ensures the user belongs to an organisation — optionally a specific org ID.

Webhook middleware

Register the webhook route before express.json() and use a raw body parser so the signature can be verified:

import express from "express";
import { verifyWebhook } from "@wocha/express";
 
const app = express();
 
app.post(
  "/webhooks/wocha",
  express.raw({ type: "application/json" }),
  verifyWebhook(process.env.WOCHA_WEBHOOK_SECRET!),
  (req, res) => {
    const event = req.wochaEvent;
    console.log(event?.event_type, event?.data);
    res.json({ received: true });
  },
);
 
app.use(express.json());

Lower-level helpers are also exported: verifyWebhookSignature(), parseWebhookEvent().

req.wocha context

After auth.middleware() runs, each request exposes:

FieldTypeDescription
userGreetUser | nullAuthenticated user claims
sessionGreetSession | nullAccess token and expiry
orgGreetOrganisation | nullActive organisation
permissionsstring[]Permissions embedded in the access token
featureFlagsRecord<string, unknown>Feature flags from the access token
isAuthenticatedbooleanWhether a valid session was resolved

Learn more

On this page