Skip to content

jwt and jwk middlewares hardcode req.url as WWW-Authenticate realm with no configurable override, unlike bearer-auth #4989

Description

@Nexory

Description

The jwt and jwk built-in middlewares produce 401 responses with a WWW-Authenticate header whose realm is unconditionally set to the full request URL (ctx.req.url):

WWW-Authenticate: Bearer realm="https://api.example.com/v1/users/42?token=abc",error="invalid_token",error_description="token verification failure"

RFC 6750 defines realm as a human-readable string that identifies the protection space — not the request URL. Reflecting the full URL here is a poor default: it conflates the request path (which varies per resource) with the realm descriptor (which should be stable and descriptive), and it produces a different realm value for every distinct URL, which is inconsistent with how realm is supposed to work.

The bearer-auth middleware avoids this by exposing a realm option (defaulting to "") that lets developers supply a fixed, meaningful string:

// bearer-auth/index.ts — line 25-26
realm?: string
// line 117
const realm = options.realm?.replace(/"/g, '\\"')

jwt and jwk have no equivalent option; their unauthorizedResponse() helper is private and always reads opts.ctx.req.url. There is no workaround short of wrapping or replacing the middleware entirely. This API inconsistency means developers using jwt/jwk cannot produce a well-formed, stable WWW-Authenticate realm value without forking the middleware.

Affected code

src/middleware/jwt/jwt.ts lines 160–173 (identical pattern at src/middleware/jwk/jwk.ts lines 170–183):

160 | function unauthorizedResponse(opts: {
161 |   ctx: Context
162 |   error: string
163 |   errDescription: string
164 |   statusText?: string
165 | }) {
166 |   return new Response('Unauthorized', {
167 |     status: 401,
168 |     statusText: opts.statusText,
169 |     headers: {
170 |       'WWW-Authenticate': `Bearer realm="${opts.ctx.req.url}",error="${opts.error}",error_description="${opts.errDescription}"`,
171 |     },
172 |   })
173 | }

Fix recommendation

Add an optional realm field to both the jwt() and jwk() options objects, mirroring what bearer-auth already does. When realm is not provided, fall back to "" rather than ctx.req.url:

// jwt options type — add:
realm?: string

// In unauthorizedResponse, replace:
`Bearer realm="${opts.ctx.req.url}"`
// with:
`Bearer realm="${opts.realm ?? ''}"`

This change is backwards-compatible. The same change should be applied to src/middleware/jwk/jwk.ts, which contains an identical copy of unauthorizedResponse.

PoC

See poc_idx0.mjs / poc_output_idx0.txt

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions