Skip to main content
Every framework adapter wraps one handler from core, createTheAuthHandler in @glinr/theauth/http, so every adapter serves the same routes with the same status codes. Paths are relative to the mount path (default /api/theauth).

Routes

Generated from listRoutes(). Call it yourself to render the table or build an OpenAPI document:

Auth levels

Status codes

Management errors use { "error": { "code", "message" } }. MCP endpoints use the OAuth shape { "error", "error_description" }.

Options

createTheAuthHandler(theauth, options) takes the shared security options plus:

Cache headers

Every response the handler produces carries Cache-Control: no-store and Pragma: no-cache, because auth responses hold credentials or session state: tokens, introspection results, authorize redirects, errors from guarded routes and anything that sets a cookie. The default is applied in one place; a route that sets its own Cache-Control keeps it (GET /mcp/jwks stays public, max-age=300). A 404 for a path the handler does not own is left alone.

Security headers

Off by default. Pass securityHeaders: true (or an options object) to add: HSTS is sent when the request URL is https, or when trustedProxy is configured and the proxy reports X-Forwarded-Proto: https. Without trustedProxy the forwarded header is ignored, so a client cannot trigger it. The core routes return JSON, so frame and CSP headers matter for HTML that plugins return. A header the route already set is never overwritten. Each header takes an override or false (hsts, referrerPolicy, frameOptions, contentSecurityPolicy, crossOriginResourcePolicy). For responses you build yourself:

CORS

Off by default: no CORS header is sent anywhere. (Earlier versions sent Access-Control-Allow-Origin: * on every MCP route, token endpoint included.) Browser based MCP clients need the discovery documents, so list the origins that may read them:
Rules, all enforced:
  • Origins are matched exactly (scheme, host, port). An origin that is not listed gets no Access-Control-Allow-Origin; it is never reflected. Vary: Origin is always set when a group has an allowlist.
  • "*" is accepted only for public documents. It cannot be combined with credentials: true, and cannot be used for token, introspect, revoke or management: the handler throws at startup.
  • A preflight (OPTIONS with Access-Control-Request-Method) is answered before authentication. It lists the methods that path serves, allowedHeaders (default Content-Type, Authorization, DPoP) and maxAge (default 600 seconds). A disallowed origin or method gets a bare 204.
  • exposedHeaders defaults to DPoP-Nonce, WWW-Authenticate.
/mcp/authorize is a browser navigation and never gets CORS. Opt a group in with groups: { token: true } (uses origins) or groups: { token: ['https://spa.example.com'] } (its own list). Token, introspection and revoke are client credential endpoints for servers; enable them for a browser only for a public client you control. At startup the handler logs a warning through the observability logger (console when none is set) when allowUnauthenticated is on, or when CORS allows "*" or opens a credential carrying group.

Guarding your own routes

requireAgent authenticates an agent token and checks one action. It never compares reason strings, it branches on the decision code.

Node bridge

Express, Fastify and NestJS use Node’s http objects. toWebRequest(req, { trustProxy }) and sendWebResponse(res, response) convert both ways, so those adapters are a few lines around the same handler.

Client IP and peer address

The client IP feeds per-IP rate limits, ipAllowlist permissions and audit rows. It is resolved in one place:
  • With trustedProxy set (trustedProxyCount or trustedHeader), the proxy headers decide, exactly as before.
  • Without it, forwarded headers are ignored and the socket’s remote address is used.
toWebRequest records the socket address on the Request out of band (never in a header, so a client cannot spoof it). Read it with peerAddressOf(request) from @glinr/theauth/http. Express, Fastify and NestJS therefore have a client IP on a direct deployment. Web-standard frameworks (Hono, Next.js and similar) never go through toWebRequest, so they still need trustedProxy to know the IP. requireAgent uses the same resolver when clientIp is not given; pass trustedProxy to it when you run behind a proxy.

Owner filters

A caller limited to their own resources gets 403 with { error: { code: "FORBIDDEN", message } } when userId names someone else, on GET /audit/export, GET /audit, GET /agents and the dashboard lists. The filter is never silently dropped.
Last modified on October 18, 2026