Auth-cohesive slice
Think of it like…a building lobby that does three things most don't. It checks not just your badge but who issued it (JWKS lookup bykid), revalidates that you still work here on every request (theverifyhook), and refuses to cache the "denied" answer at the elevator, so a fired employee can't ride up tomorrow on a stale 401 (Cache-Control: no-store).
DaloyJS is a Relying Party, not an auth server
DaloyJS validates tokens; it does not mint user sessions through an authorization-code flow, host a consent screen, or store user/client credentials. Pair these middleware with a dedicated identity provider:
- Hosted IdPs: Auth0, Okta, Azure AD / Entra ID, AWS Cognito, Google Identity, Clerk, WorkOS, Supabase Auth, Logto, Stytch, Kinde. Anything that publishes a standard
/.well-known/jwks.jsonworks withjwk()out of the box. - Self-hosted IdPs: Keycloak, Ory Hydra, ZITADEL, Authentik, Dex. Same JWKS contract, same one-line
jwk()setup. - Your own sibling auth service: a separate DaloyJS app using
createJwtSigner()to mint tokens and exposing a JWKS endpoint. The API service then validates those tokens withjwk()exactly as it would for an external IdP.
Rough mapping of which middleware to reach for:
- Browser app + external IdP (OIDC):
jwk()on the API,requireScopes()per route,session()only if you also need server-side session state alongside the access token. - Service-to-service inside one tenant:
bearerAuth({ validate })with an opaque token, orjwk()if both sides already speak JWT. The internal-service preset relaxes browser-only headers for these endpoints. - Webhook receivers: neither
bearerAuth()norjwk(); use the dedicated HMAC verifier (see the security overview). - Admin tools / scripts:
basicAuth()behindipRestriction(), or short-lived JWTs from your IdP.
Daloy closes the auth-cohesive subset of the leftover items from the secure-by-default initiative. Each one is additive and opt-in:
jwk(): asymmetric-only Bearer-token middleware backed by a JWKS source. RefusesHS*at construction, requires akidheader that matches a JWK in the set, and cross-checks JWT-headeralgagainst the JWK's declaredalgwhen both are present.bearerAuth({ verify })/jwk({ verify }): per-request revalidation hook so revocation lists, token-version counters, and "user changed password since this token was issued" checks can invalidate previously-issued credentials.basicAuth({ onAuthSuccess }): typed-context callback that fires afterctx.state.user.usernameis stamped, so handlers do not re-parse theAuthorizationheader.Cache-Control: no-storeon every first-party auth helper401challenge (bearerAuth(),basicAuth(),jwk()) so intermediaries never cache an auth challenge, RFC 9111 §3.5 and audit alignment.
1. jwk() middleware
Drop-in Bearer-token middleware backed by a JWKS source. The algorithm allowlist is intentionally narrow: only RS256 / RS384 / RS512, PS256 / PS384 / PS512, ES256 / ES384 / ES512, and EdDSA. Symmetric HS*algorithms are refused at construction, the classic confused-deputy "HS256 verified with the JWKS public key as the HMAC secret" attack cannot be configured. The middleware is exported from the dedicated subpath @daloyjs/core/jwk.
- 01requestClientjwk() middlewareRequest with Bearer tokenJWT header carries kid + alg
- 02asyncjwk() middlewareIdP JWKSFetch key set by kidTTL-cached, in-flight dedup, stale fallback
- 03notejwk() middlewarejwk() middlewareCross-check JWT alg vs JWK alg; reject HS*asymmetric-only allowlist
- 04responsejwk() middlewareClientVerify signature + exp, stamp ctx.state.user{ sub, scopes, claims }
jwks accepts a static JwkSet, an https:// URL (with TTL caching and in-flight-promise dedup so a thundering-herd of concurrent requests resolves into a single fetch), or a custom resolver function. http:// JWKS URLs and non-finite / negative fetchTtlSeconds / maxStaleSeconds are refused at construction. The middleware stamps ctx.state.user = { sub, scopes, claims }; the scope normalizer reads scope (RFC 6749 space-separated string), scp (Azure AD array), and scopes (array) claims and dedupes the result.
When the JWKS source is a URL, a TTL-expiry refresh that fails (network error, non-2xx, or malformed body) does not take down every request: the last successfully fetched key set keeps serving for a bounded maxStaleSeconds grace window (default 3600, set 0 to disable) on top of fetchTtlSeconds. The very first fetch is never eligible for this fallback, so an unreachable IdP at boot still fails closed, and tokens are always cryptographically verified and exp-checked regardless.
2. Per-scheme verify(credentials, ctx) hook
Both bearerAuth() and jwk() accept an optional verify callback that runs after the static validate / signature check passes. Returning false throws ForbiddenError (403, no WWW-Authenticate per RFC 6750); returning true or undefined accepts. Use it to consult a revocation list, a token-version counter, or any other per-request signal that a previously-issued token has been invalidated.
- 01staticvalidate / signature checktoken shape or JWT signature + exp
- 02revalidateverify(credentials, ctx)revocation list, token-version, password-changed
- 03returns falseForbiddenError403, no WWW-Authenticate (RFC 6750)
- 04true / undefinedRequest acceptedhandler runs
3. basicAuth({ onAuthSuccess })
Fires once ctx.state.user.username has been stamped, with the typed (credentials, ctx) tuple. The previous idiomatic workaround was a separate beforeHandle that re-parsed the Authorization header in every handler; that is no longer necessary.
4. Cache-Control: no-store on auth 401 challenges
Every first-party auth helper now emits Cache-Control: no-store alongside WWW-Authenticate on the 401 response. A shared CDN, a corporate proxy, or a service-worker cache could previously cache the challenge and serve it to a different user;no-store closes that fingerprinting and stale-challenge risk. This applies uniformly to bearerAuth(), basicAuth(), and the new jwk().
What shipped next
The remaining leftover items, the wsRateLimit() adapter, loginThrottle() preset, rotateSession() helper, the file-upload MIME + magic-byte + size guard, the requirePayloadAuth scheme flag, and the WebSocket-helper safe defaults, shipped in the 0.23.0 remaining slice.