You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The core auth middleware recognizes exactly three API credentials: a session cookie, a personal access token (ec_pat_*), and an EmDash-issued OAuth token (ec_oat_*). There's no way for a package to add a credential type. Anything else on /_emdash/api/* is rejected before package code runs.
authProviders solved this for humans: a package can add a login method without core knowing about it. There's no equivalent for API clients. Concretely, I'm building an AAuth integration — agents authenticate with RFC 9421 HTTP Message Signatures carrying a token in the Signature-Key header (no Authorization: Bearer at all). Today the only options are a parallel mirror of the REST API under a different prefix (duplicating the route surface and drifting from the OpenAPI doc), or fronting the site with a proxy that flattens everyone onto one PAT.
The same gap applies to any externally issued credential: mTLS-bound tokens, SAML-derived JWTs, enterprise gateway schemes.
Proposal
A tokenVerifiers config option, following the authProviders descriptor + virtual-module pattern:
The rejection-object form lets a verifier put an authentication challenge on the 401 (a WWW-Authenticate value, or a scheme header like AAuth's AAuth-Requirement) so clients learn how to retry — the same idea as the MCP endpoint's OAuth discovery WWW-Authenticate, which still wins on that endpoint.
Middleware semantics (all in the existing handleBearerAuth path):
Built-in tokens resolve first, unchanged. A failed ec_pat_*/ec_oat_* is still rejected without consulting verifiers — that namespace stays core's.
Verifiers run when built-ins don't match: no Bearer header, or an unrecognized Bearer prefix. First verifier to resolve wins; "none" means "not my credential type, try the next"; "invalid" (or a throw) rejects with 401 — a bad credential is never downgraded to anonymous.
A verified request resolves to an existing user + scope strings and then flows through the existing scope enforcement (enforceTokenScope), role floors, disabled-user checks, and the MCP endpoint's bearer-only gate — identical to PAT/OAuth auth. Verifiers grant nothing; they only authenticate.
CSRF is skipped exactly as for other token auth, so verifiers are documented as per-request-credential only (signed/custom headers, never cookies).
With no tokenVerifiers configured, the generated virtual module is an empty array and behavior is byte-for-byte unchanged.
Why in core rather than a plugin or proxy
Sandboxed plugins can't do this by design (no user-write, no middleware position). A parallel route tree works but duplicates every API path and splits the OpenAPI story. This is the same shape as authProviders and Astro's own adapter hooks: core owns the pipeline, packages own the credential.
What I have — running in production
The implementation is done, tested, and serving production traffic: blog.hello.coop (the Hellō blog) now runs on EmDash with this hook authenticating RFC 9421-signed agent requests — standing grants for routine writes, single-use per-call tokens for publish/delete, challenge headers on rejections. The site is served at blog.hello.coop (a Cloudflare for SaaS custom hostname); the same worker also answers on its workers.dev and staging hostnames, which exercised the per-request publicOrigin context — the verifier binds tokens to the host each request actually arrived on.
The branch is additive: one new function in the middleware plus virtual-module plumbing, 15 middleware tests, a docs section in the authentication guide, changeset (minor). Two small siblings from the same use case, filed separately: exporting generateOpenApiDocument (so an integration can serve an annotated copy of the API spec), and — future, separate discussion — login-page method configuration. Happy to open the PR if this direction works for you, and equally happy to adjust the interface (naming, context shape, whether verifiers should see unrecognized Bearer prefixes) to taste.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
The problem
The core auth middleware recognizes exactly three API credentials: a session cookie, a personal access token (
ec_pat_*), and an EmDash-issued OAuth token (ec_oat_*). There's no way for a package to add a credential type. Anything else on/_emdash/api/*is rejected before package code runs.authProviderssolved this for humans: a package can add a login method without core knowing about it. There's no equivalent for API clients. Concretely, I'm building an AAuth integration — agents authenticate with RFC 9421 HTTP Message Signatures carrying a token in theSignature-Keyheader (noAuthorization: Bearerat all). Today the only options are a parallel mirror of the REST API under a different prefix (duplicating the route surface and drifting from the OpenAPI doc), or fronting the site with a proxy that flattens everyone onto one PAT.The same gap applies to any externally issued credential: mTLS-bound tokens, SAML-derived JWTs, enterprise gateway schemes.
Proposal
A
tokenVerifiersconfig option, following theauthProvidersdescriptor + virtual-module pattern:The entrypoint exports one function:
The rejection-object form lets a verifier put an authentication challenge on the 401 (a
WWW-Authenticatevalue, or a scheme header like AAuth'sAAuth-Requirement) so clients learn how to retry — the same idea as the MCP endpoint's OAuth discoveryWWW-Authenticate, which still wins on that endpoint.Middleware semantics (all in the existing
handleBearerAuthpath):ec_pat_*/ec_oat_*is still rejected without consulting verifiers — that namespace stays core's."none"means "not my credential type, try the next";"invalid"(or a throw) rejects with 401 — a bad credential is never downgraded to anonymous.enforceTokenScope), role floors, disabled-user checks, and the MCP endpoint's bearer-only gate — identical to PAT/OAuth auth. Verifiers grant nothing; they only authenticate.With no
tokenVerifiersconfigured, the generated virtual module is an empty array and behavior is byte-for-byte unchanged.Why in core rather than a plugin or proxy
Sandboxed plugins can't do this by design (no user-write, no middleware position). A parallel route tree works but duplicates every API path and splits the OpenAPI story. This is the same shape as
authProvidersand Astro's own adapter hooks: core owns the pipeline, packages own the credential.What I have — running in production
The implementation is done, tested, and serving production traffic: blog.hello.coop (the Hellō blog) now runs on EmDash with this hook authenticating RFC 9421-signed agent requests — standing grants for routine writes, single-use per-call tokens for publish/delete, challenge headers on rejections. The site is served at blog.hello.coop (a Cloudflare for SaaS custom hostname); the same worker also answers on its workers.dev and staging hostnames, which exercised the per-request
publicOrigincontext — the verifier binds tokens to the host each request actually arrived on.The branch is additive: one new function in the middleware plus virtual-module plumbing, 15 middleware tests, a docs section in the authentication guide, changeset (minor). Two small siblings from the same use case, filed separately: exporting
generateOpenApiDocument(so an integration can serve an annotated copy of the API spec), and — future, separate discussion — login-page method configuration. Happy to open the PR if this direction works for you, and equally happy to adjust the interface (naming, context shape, whether verifiers should see unrecognized Bearer prefixes) to taste.All reactions