Skip to content

fix(server): apiKeyAuth no longer mutates immutable Request headers (Workers-compat)#27

Merged
kjgbot merged 1 commit intomainfrom
fix/apikey-auth-workers-immutable-headers
Apr 23, 2026
Merged

fix(server): apiKeyAuth no longer mutates immutable Request headers (Workers-compat)#27
kjgbot merged 1 commit intomainfrom
fix/apikey-auth-workers-immutable-headers

Conversation

@kjgbot
Copy link
Copy Markdown
Contributor

@kjgbot kjgbot commented Apr 23, 2026

Summary

Fixes a production bug where apiKeyAuth() middleware throws
TypeError: Can't modify immutable headers on every x-api-key
request in Cloudflare Workers. Surfaced 2026-04-23 when cloud's
sage worker started using x-api-key to mint bearer tokens;
every call returned 500.

Previous implementation rewrote c.req.raw.headers.set("authorization", ...)
after successful api-key auth so downstream helpers reading the
Authorization header would see a synthesized bearer. That works
in Node but not in Workers (Request headers are locked).

Fix

Instead of mutating the Request headers, the middleware now stores
the authenticated claims on Hono's context:

c.set("apiKeyClaims", auth.claims);
c.set("apiKeyVia", "api_key");

Downstream auth helpers (authenticateAndAuthorize, etc.) check
the context first before falling back to header parsing. No more
in-place Request mutation.

Concrete changes:

  • packages/server/src/middleware/api-key-auth.ts — drop header rewrite;
    set apiKeyClaims/apiKeyVia on context. Delete the now-unused
    signHs256Token + encodeBase64Url helpers.
  • packages/server/src/env.ts — add apiKeyClaims / apiKeyVia to
    AppEnv["Variables"].
  • packages/server/src/lib/auth.ts — add authenticateFromContext() and
    authenticateAndAuthorizeFromContext() helpers that prefer
    context-stashed claims, falling back to header parsing. Existing
    authenticate() / authenticateAndAuthorize() signatures preserved
    for backward compat.
  • packages/server/src/routes/{tokens,identities,role-assignments}.ts
    swap to the context-based helpers on routes mounted behind apiKeyAuth().
  • packages/server/src/middleware/scope.tsrequireScope/requireScopes
    now prefer c.get("apiKeyClaims") over parsing the Authorization bearer.
  • packages/server/src/server.ts — global gate now admits a request when
    c.get("apiKeyVia") === "api_key". It still refuses to read raw
    x-api-key, so un-mounted routes cannot silently accept unvalidated keys.

Bearer-wins precedence is preserved (handled inside
authenticateBearerOrApiKey — bearer is checked first, and only on failure
does it consider the api-key). All existing tests updated, new regression
tests added.

New tests

packages/server/src/__tests__/api-key-auth-middleware.test.ts:

  • Source-level guard — reads api-key-auth.ts and asserts (after stripping
    comments) that its code contains no .headers.set(, no c.req.raw.headers,
    and no signHs256Token( call. Prevents a silent regression.
  • Locked-headers Request for POST /v1/identities — freezes
    Headers.set/delete/append on the Request to mirror Workers' immutable
    headers and asserts the x-api-key-authenticated request does not 500.
  • Locked-headers Request for POST /v1/tokens — same, on the exact codepath
    cloud's sage worker hits in production.
  • Bearer-wins precedence — mixed bearer + x-api-key request, bearer with
    manage scope wins over read-only api-key; was previously tested via the
    header-rewrite path.

Test plan

  • node --test --import tsx 'packages/server/src/__tests__/*.test.ts' — 328 pass (was 324; +4 new)
  • npx turbo typecheck --filter=@relayauth/server — clean
  • Regression test confirms no .headers.set( or c.req.raw.headers calls remain in middleware source

Bumps

  • @relayauth/server: 0.2.0 -> 0.2.1

After this merges, @relayauth/server@0.2.1 needs publishing to npm
and cloud's packages/relayauth/package.json bumped to consume it.

Scope

  • Does NOT touch phase-122 / RS256 cutover — orthogonal fix in a different file.
  • No breaking change to the public surface of @relayauth/server. External
    consumers of authenticate() / authenticateAndAuthorize() see no API change;
    the new *FromContext variants are additive.
  • Does not touch @relayauth/sdk — server-only.
  • Does not change the dev-token script.

🤖 Generated with Claude Code

…Workers-compat)

The apiKeyAuth() middleware previously rewrote c.req.raw.headers.set(
"authorization", ...) after successfully authenticating an x-api-key, so the
downstream global gate and route handlers would see a synthesized Bearer.
This worked in Node but threw TypeError: Can't modify immutable headers in
Cloudflare Workers on every x-api-key request, 500ing POST /v1/tokens and
POST /v1/api-keys in production.

Replace the header rewrite with context-based signaling:
- apiKeyAuth() now calls c.set("apiKeyClaims", auth.claims) /
  c.set("apiKeyVia", "api_key") and drops the synthesized bearer path.
- AppEnv["Variables"] gains apiKeyClaims / apiKeyVia typing.
- New lib/auth helpers authenticateFromContext() and
  authenticateAndAuthorizeFromContext() prefer context-stashed claims,
  falling back to Authorization header parsing otherwise.
- Routes mounted behind apiKeyAuth (tokens, identities, role-assignments,
  plus the scope.ts requireScope middleware) now consult the context.
- The server.ts global gate admits requests when apiKeyVia === "api_key"
  without falling back to reading the raw x-api-key header, preserving the
  invariant that un-mounted routes reject raw x-api-key.

Added regression tests in api-key-auth-middleware.test.ts:
- Static source-level guard that the middleware source contains no
  .headers.set( or c.req.raw.headers references or signHs256Token() calls.
- End-to-end tests that freeze Headers.set/delete/append on a Request to
  simulate Workers' locked headers and assert POST /v1/identities and
  POST /v1/tokens both succeed (reproducing the original 500).
- Bearer-wins precedence test for a mixed bearer + x-api-key request via
  the new context path.

All 324 existing tests still pass; total 328.

Bumps @relayauth/server 0.2.0 -> 0.2.1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kjgbot kjgbot merged commit 31451f4 into main Apr 23, 2026
2 checks passed
@kjgbot kjgbot deleted the fix/apikey-auth-workers-immutable-headers branch April 23, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant