JWT and authentication safeguards
The authentication slice resolves signing keys bykid, runs theverifyhook on every request, and addsCache-Control: no-storeto authentication failures.
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 provides a cohesive set of authentication safeguards. 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. These callbacks run in preBody: raw route/header context is available, but ctx.body is always undefined so an unauthenticated upload can be rejected before it is consumed.
- 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. The callback runs in preBody, so move any logic that requires a validated request body into a later beforeHandle.
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().
Related auth safeguards
Related protections include 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, are covered in WebSocket and login safeguards.