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
Description
The
jwtandjwkbuilt-in middlewares produce401responses with aWWW-Authenticateheader whoserealmis unconditionally set to the full request URL (ctx.req.url):RFC 6750 defines
realmas 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 differentrealmvalue for every distinct URL, which is inconsistent with how realm is supposed to work.The
bearer-authmiddleware avoids this by exposing arealmoption (defaulting to"") that lets developers supply a fixed, meaningful string:jwtandjwkhave no equivalent option; theirunauthorizedResponse()helper is private and always readsopts.ctx.req.url. There is no workaround short of wrapping or replacing the middleware entirely. This API inconsistency means developers usingjwt/jwkcannot produce a well-formed, stableWWW-Authenticaterealm value without forking the middleware.Affected code
src/middleware/jwt/jwt.tslines 160–173 (identical pattern atsrc/middleware/jwk/jwk.tslines 170–183):Fix recommendation
Add an optional
realmfield to both thejwt()andjwk()options objects, mirroring whatbearer-authalready does. Whenrealmis not provided, fall back to""rather thanctx.req.url:This change is backwards-compatible. The same change should be applied to
src/middleware/jwk/jwk.ts, which contains an identical copy ofunauthorizedResponse.PoC
See poc_idx0.mjs / poc_output_idx0.txt