Search docs

Jump between documentation pages.

Protect a DaloyJS API with Auth0

Auth0 is a developer-friendly IdP that handles universal login, MFA, social login, and a rich rule/action engine. For backend protection, Auth0's official quickstart uses express-oauth2-jwt-bearer, which is Express-only. DaloyJS isn't Express, so we use the same primitive — JWT verification against Auth0's JWKS — through jose. That keeps the same security guarantees while running on every runtime DaloyJS targets, including the edge.

1. Configure an Auth0 API

  1. In the Auth0 dashboard, go to Applications → APIs → Create API. Pick an identifier (e.g. https://api.acme.example.com) — this becomes the aud claim on issued tokens.
  2. Define permissions (e.g. read:items, write:items) and enable RBAC + Add Permissions in the Access Token if you want them in permissions.
  3. Note your tenant's domain (e.g. dev-abc123.us.auth0.com). Your issuer is https://{domain}/ (trailing slash).

2. Install

ts
pnpm add jose

3. Environment variables

ts
# .env
AUTH0_DOMAIN=dev-abc123.us.auth0.com
AUTH0_AUDIENCE=https://api.acme.example.com
AUTH0_REQUIRED_SCOPE=read:items

4. Plugin

ts
// src/plugins/auth0.ts
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
import type { App } from "@daloyjs/core";

const issuer = `https://${process.env.AUTH0_DOMAIN}/`;
const audience = process.env.AUTH0_AUDIENCE!;

const jwks = createRemoteJWKSet(
  new URL(`https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`),
);

export interface Principal {
  sub: string;
  scopes: string[];
  permissions: string[];
  claims: JWTPayload;
}

export const auth0Plugin = {
  name: "auth0",
  register(app: App) {
    app.decorate("verifier", {
      async verify(token: string): Promise<Principal> {
        const { payload } = await jwtVerify(token, jwks, {
          issuer,
          audience,
          algorithms: ["RS256"],
        });
        const scopes =
          typeof payload.scope === "string" ? payload.scope.split(" ") : [];
        const permissions = Array.isArray(payload.permissions)
          ? (payload.permissions as string[])
          : [];
        return { sub: String(payload.sub), scopes, permissions, claims: payload };
      },
    });
  },
};

declare module "@daloyjs/core" {
  interface AppState {
    verifier: { verify(token: string): Promise<Principal> };
    principal?: Principal;
  }
}

5. Guard a route

ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { auth0Plugin } from "./plugins/auth0";
import { requireAuth } from "./plugins/auth"; // from the Overview page

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
app.register(auth0Plugin);

app.route({
  method: "GET",
  path: "/items",
  operationId: "listItems",
  middleware: [requireAuth(process.env.AUTH0_REQUIRED_SCOPE!)],
  responses: {
    200: { description: "OK", body: z.object({ user: z.string(), items: z.array(z.string()) }) },
  },
  handler: ({ state }) => ({
    status: 200,
    body: { user: state.principal!.sub, items: ["a", "b"] },
  }),
});

Permissions (RBAC)

If you enabled Add Permissions in the Access Token, the JWT carries a permissions array. Tighten the overview's requireAuth to check principal.permissions when you want to enforce role assignments rather than OAuth 2.0 scopes:

ts
export function requirePermission(...perms: string[]): Middleware {
  return async (ctx, next) => {
    // ... bearer extraction & verify() as before ...
    const have = ctx.state.principal!.permissions;
    if (!perms.every((p) => have.includes(p))) {
      return ctx.problem(403, "forbidden", "Missing permission");
    }
    return next();
  };
}

Auth0 Actions & custom claims

Add custom claims through an Action on the Login flow. Namespace them (e.g. https://acme.example.com/tenant) per Auth0's rules — that prevents collisions with standard claims and is required for non-reserved claims to be included.

Runtimes

jose uses Web Crypto, so this setup runs on Node 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and AWS Lambda. No need to swap libraries between environments.

Notes

  • The iss claim Auth0 issues includes a trailing slash. Mismatching it (with or without the slash) is a common cause of validation failures.
  • Set a non-empty audienceon the API — without it Auth0 returns an opaque token that you can't verify locally.
  • For sensitive operations, also check Auth0's azp (authorized party) claim against your allowed client IDs.

See also Okta, Clerk, and the auth integrations overview.