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.
- 01requestClient appAuth0 tenantUniversal Login (authorization-code + PKCE)Auth0 owns login, MFA, social
- 02responseAuth0 tenantClient appAccess token (RS256 JWT, aud = your API)
- 03requestClient appDaloyJS APIRequest with Authorization: Bearer <token>
- 04asyncDaloyJS APIAuth0 JWKSFetch signing keys (cached by jwk())GET /.well-known/jwks.json
- 05responseDaloyJS APIClient appjwk() checks iss (trailing slash), aud, RS256, then scopes401 on a bad token, 403 on a missing scope
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
Nothing beyond @daloyjs/core: jwk(), requireScopes(), and requireRoles() are built in.
3. Environment variables
4. Verify tokens
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
Permissions (RBAC)
With RBAC and Add Permissions in the Access Token enabled, the token carries a permissions array. Enforce it with requireRoles():
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
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 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, injwk({ verify: (claims) => allowedClients.includes(String(claims.azp)) }). Afalseanswers403.
See also Okta, Clerk, and the auth integrations overview.