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
- In the Auth0 dashboard, go to Applications → APIs → Create API. Pick an identifier (e.g.
https://api.acme.example.com) — this becomes theaudclaim on issued tokens. - Define permissions (e.g.
read:items,write:items) and enable RBAC + Add Permissions in the Access Token if you want them inpermissions. - Note your tenant's domain (e.g.
dev-abc123.us.auth0.com). Your issuer ishttps://{domain}/(trailing slash).
2. Install
pnpm add jose3. Environment variables
# .env
AUTH0_DOMAIN=dev-abc123.us.auth0.com
AUTH0_AUDIENCE=https://api.acme.example.com
AUTH0_REQUIRED_SCOPE=read:items4. Plugin
// 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
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:
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
issclaim 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.