Skip to content

Search docs

Jump between documentation pages.

Browse docs

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, and it doesn't need it: the built-in jwk() does the same job, JWT verification against Auth0's JWKS, with no extra package and on every runtime DaloyJS targets, including the edge.

Auth0 access-token verification
Client appAuth0 tenantDaloyJS APIAuth0 JWKS
  1. 01requestClient appAuth0 tenantUniversal Login (authorization-code + PKCE)Auth0 owns login, MFA, social
  2. 02responseAuth0 tenantClient appAccess token (RS256 JWT, aud = your API)
  3. 03requestClient appDaloyJS APIRequest with Authorization: Bearer <token>
  4. 04asyncDaloyJS APIAuth0 JWKSFetch signing keys (cached by jwk())GET /.well-known/jwks.json
  5. 05responseDaloyJS APIClient appjwk() checks iss (trailing slash), aud, RS256, then scopes401 on a bad token, 403 on a missing scope
DaloyJS is the resource server: jwk() verifies the RS256 token against your tenant's JWKS and enforces scopes or permissions. Auth0 owns login and is the only party that mints tokens.

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

Nothing beyond @daloyjs/core: jwk(), requireScopes(), and requireRoles() are built in.

3. Environment variables

bash
# .env
AUTH0_DOMAIN=dev-abc123.us.auth0.com
AUTH0_AUDIENCE=https://api.acme.example.com

4. Verify tokens

ts
// src/auth.ts
import { jwk } from "@daloyjs/core";

const domain = process.env.AUTH0_DOMAIN!;

export const auth0 = jwk({
  jwks: `https://${domain}/.well-known/jwks.json`,
  algorithms: ["RS256"],
  issuer: `https://${domain}/`, // the trailing slash is part of Auth0's iss
  audience: process.env.AUTH0_AUDIENCE!,
  maxLifetimeSeconds: 86_400,   // also makes `exp` mandatory
});

declare module "@daloyjs/core" {
  interface AppState {
    // What jwk() stamps on every authenticated request.
    user?: { sub?: string; scopes: string[]; claims: Record<string, unknown> };
  }
}

jwk() answers 401 with a WWW-Authenticate challenge for a missing, expired, or forged token, caches the JWKS, and picks up Auth0's key rotation on its own.

5. Guard a route

ts
import { z } from "zod";
import { App, rateLimit, requireScopes, secureHeaders } from "@daloyjs/core";
import { auth0 } from "./auth.ts";

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

app.get(
  "/items",
  {
    operationId: "listItems",
    hooks: requireScopes(["read:items"], { phase: "preBody" }),
    responses: {
      200: { description: "OK", body: z.object({ user: z.string(), items: z.array(z.string()) }) },
    },
  },
  ({ state }) => ({
    status: 200,
    body: { user: String(state.user?.sub), items: ["a", "b"] },
  }),
);

Permissions (RBAC)

With RBAC and Add Permissions in the Access Token enabled, the token carries a permissions array. Enforce it with requireRoles():

ts
import { requireRoles } from "@daloyjs/core";

app.delete(
  "/items/:id",
  {
    hooks: requireRoles(["delete:items"], { claim: "permissions" }),
    responses: { 204: { description: "Deleted" } },
  },
  () => ({ status: 204, body: undefined }),
);

Auth0 Actions & custom claims

Add custom claims through an Action on the Login flow. Namespace them (e.g. https://acme.example.com/roles) per Auth0's rules, that prevents collisions with standard claims and is required for non-reserved claims to be included. requireRoles() reads a namespaced claim by its exact name: requireRoles(["admin"], { claim: "https://acme.example.com/roles" }). Other custom claims are on ctx.state.user.claims.

Runtimes

jwk() uses fetch and Web Crypto only, so this setup runs everywhere DaloyJS does: Node.js, Bun, Deno, Cloudflare Workers, Vercel, AWS Lambda, Azure Functions, and Fastly Compute (declare your Auth0 domain as a backend there). No library swap 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 audience on 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, in jwk({ verify: (claims) => allowedClients.includes(String(claims.azp)) }). A false answers 403.

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

Edit this page on GitHub

Last updated