@nightowlsdev/auth-auth0
AuthStateless, offline-capable Auth0 AuthProvider that verifies access tokens against Auth0's public JWKS.
What it does
An AuthProvider for @nightowlsdev/core that verifies an Auth0-issued access token with jose against Auth0's public JWKS and maps it into a Night Owls AuthContext, resolving identity server-side at the request boundary (never from the body). It uses public signing keys only (no client secret, no Management API): createRemoteJWKSet is built once at factory scope and jose caches keys, then each request runs jwtVerify(token, JWKS, { issuer, audience }). The issuer is normalized to a trailing slash so both forms work. Claims map to userId (sub, required string), tenantId (orgClaim, default org_id, else 'default'), and capabilities (rolesClaim, default permissions, when an array). An optional allowedOrgs allow-list fails closed: a token with no/non-string org claim is rejected when the list is set. The jwks resolver is injectable for hermetic RS256 tests with no network. Construct with auth0Auth({ issuerBaseUrl, audience, ... }) and pass to a runner. Any verification failure returns null, yielding a 401. Also exports nightOwlsPlugin, the CLI plugin manifest.
Install
pnpm add @nightowlsdev/auth-auth0Key exports
- auth0Auth
- nightOwlsPlugin
- Auth0AuthOpts (type)
Usage
import { auth0Auth } from "@nightowlsdev/auth-auth0";
import { createNextjsRunner } from "@nightowlsdev/runner-nextjs";
const auth = auth0Auth({
issuerBaseUrl: process.env.AUTH0_ISSUER_BASE_URL!,
audience: process.env.AUTH0_AUDIENCE!,
});
const runner = createNextjsRunner({ engine, auth, storage });What it provides
auth-auth0 is the AuthProvider for @nightowlsdev/core that verifies an Auth0-issued access token against Auth0's public JWKS with jose and maps it into a Night Owls AuthContext ({ tenantId, userId, capabilities, orgScope? }). It is stateless and offline-capable — no Auth0 Management API, no client secret, no per-request round-trip (jose builds the JWKS resolver once and caches the keys). Identity is resolved server-side at the request boundary, never from the request body.
When to use it
- Your users sign in through Auth0 and you want that access token to govern a Night Owls swarm.
- You want low-latency, offline verification: the token is checked cryptographically against cached JWKS, with no call back to the IdP per request.
- You want a fail-closed org allow-list — allowedOrgs rejects any token whose org claim is missing, non-string, or off the list.
- You are using the FR-055 sub-org scope axis and want it resolved from a trusted JWT claim.
When not to
- You use Supabase Auth — reach for auth-supabase.
- You sign your own tokens or want no vendor dependency — use auth-jwt.
- You must revoke a token before it expires — JWKS verification cannot know a token was revoked; add auth-sessions or keep TTLs short.
Alternatives
- auth-supabaseYour IdP is Supabase. It validates against Supabase per request (a real revocation check) at the cost of a network round-trip; this package trades that away for offline speed.
- auth-jwtYou want no jose dependency and full control: verifyJwtWithJwks handles ES256/RS256 against a JWKS you fetch and cache yourself, and you assemble the AuthProvider.
- A custom AuthProviderYour claims or IdP differ from Auth0's conventions. AuthProvider is a single authenticate(req) method; implement it directly.
Strengths
- Stateless and offline: createRemoteJWKSet is built once at factory scope and jose caches the keys, so verification is cryptographic with no per-request call to Auth0 — lower latency, no availability coupling.
- Public signing keys only: no client secret, no Management API credential ever enters the process.
- allowedOrgs is a fail-closed allow-list — a token with no/non-string org claim is rejected when the list is set, never admitted into the 'default' tenant.
- The issuer is normalized to a trailing slash once at construction, so both https://tenant.auth0.com and the trailing-slash form work and jwtVerify's issuer check matches the token exactly.
- The jwks resolver is injectable, so RS256/ES256 verification is hermetically testable with no network.
Limits & trade-offs
- No revocation: an access token is valid until it expires, and JWKS verification cannot detect a revoked one — pair auth-sessions or use short TTLs.
- Auth0-specific claim conventions (sub / org / permissions), with jose as a direct dependency.
- This path is asymmetric only — it verifies a provider's ES256/RS256 access token; it does not sign or manage login/logout.
- It maps identity; it is not a session store or a login flow.
How it works
auth0Auth({ issuerBaseUrl, audience, ... }) normalizes the issuer to a trailing slash and builds JWKS = createRemoteJWKSet(issuer + '.well-known/jwks.json') once at factory scope (jose caches the keys). Each request, authenticate parses Authorization: Bearer <jwt> and runs jwtVerify(token, JWKS, { issuer, audience }); any failure — bad signature, expiry, wrong issuer/audience — returns null. From a valid payload it maps userId from sub (a required string; a non-string returns null), tenantId from orgClaim (default org_id, else 'default'), capabilities from rolesClaim (default permissions, when an array), and the FR-055 org_scope claim (a present-but-malformed scope rejects the token). When allowedOrgs is set, only a string org claim on the list is admitted. A null result yields a 401 from the runner.
Examples
Wire it into a Next.js runner
Public JWKS verification — no client secret, no round-trip to Auth0 per request.
import { auth0Auth } from "@nightowlsdev/auth-auth0";
import { createNextjsRunner } from "@nightowlsdev/runner-nextjs";
const auth = auth0Auth({
issuerBaseUrl: process.env.AUTH0_ISSUER_BASE_URL!, // trailing slash optional — normalized once
audience: process.env.AUTH0_AUDIENCE!,
});
const runner = createNextjsRunner({ engine, auth, storage });Custom claims + a fail-closed org allow-list
A token whose org claim is missing or off the list is rejected — never admitted into 'default'.
import { auth0Auth } from "@nightowlsdev/auth-auth0";
const auth = auth0Auth({
issuerBaseUrl: process.env.AUTH0_ISSUER_BASE_URL!,
audience: process.env.AUTH0_AUDIENCE!,
orgClaim: "org_id",
rolesClaim: "permissions",
allowedOrgs: ["org_acme", "org_globex"], // only these orgs may enter
});Doing the parts it doesn't support
- Revoking a token before it expiresJWKS verification is offline and cannot see a revocation. Mint jti-bearing sessions and gate them with auth-sessions' isLive, or keep the access-token TTL short and rely on refresh.
- Verifying without the jose dependencyUse auth-jwt: verifyJwtWithJwks(token, jwks, { nowSec }) validates ES256/RS256 on node:crypto only — you fetch and cache the JWKS yourself and assemble the AuthProvider.
Related
- auth-supabase — The sibling adapter for Supabase sessions — validates per request, catching revocation, at the cost of a round-trip.
- auth-jwt — Dependency-free ES256/RS256 verification if you want no jose and to own the JWKS fetch.
- auth-sessions — Add revocation on top — logout and admin kill without shortening the access-token TTL.
- core — Defines the AuthProvider / AuthContext seam this package implements.
- org-scope — The FR-055 sub-org scope axis this provider resolves from the org_scope claim.