Skip to content

fix(server): suppress HS256 JWKS entry when SIGNING_KEY is unbound#32

Merged
khaliqgant merged 1 commit intomainfrom
fix/jwks-suppress-hs256-when-unconfigured
Apr 23, 2026
Merged

fix(server): suppress HS256 JWKS entry when SIGNING_KEY is unbound#32
khaliqgant merged 1 commit intomainfrom
fix/jwks-suppress-hs256-when-unconfigured

Conversation

@kjgbot
Copy link
Copy Markdown
Contributor

@kjgbot kjgbot commented Apr 23, 2026

Summary

Same-day follow-up to phase 122 step 3 (cloud#299, merged + deployed today), which removed the SIGNING_KEY worker binding on the relayauth worker and set RELAYAUTH_VERIFIER_ACCEPT_HS256=false on sage + specialist-worker.

The runbook's exit criterion for step 3 is that JWKS at https://api.relayauth.dev/.well-known/jwks.json publishes RSA only. It still publishes HS256:

$ curl -s https://api.relayauth.dev/.well-known/jwks.json
{"keys":[
  {"kty":"oct","use":"sig","alg":"HS256","kid":"production"},
  {"kty":"RSA","use":"sig","alg":"RS256","kid":"lU6MbIqsOP_O7ONvRLMNlGMFW9ET5KxU5NfIhuhrb18", "n":"...","e":"AQAB"}
]}

Root cause

packages/server/src/routes/jwks.ts hardcoded the HS256 entry into the keys array, gated only on SIGNING_KEY_ID (still present for legacy kid accounting), not on SIGNING_KEY (which #299 removed). So the worker can't sign or verify HS256 anymore — there is no key material — but JWKS still advertises HS256 as a supported algorithm.

Cosmetic, not a security regression

The entry has no k field (the actual HMAC secret); the old comment explicitly said "never the symmetric key material." No verifier can validate HS256 tokens against this entry. The migration's security objective (no downstream service will accept HS256) is already met by RELAYAUTH_VERIFIER_ACCEPT_HS256=false on sage + specialist-worker.

That said, the runbook exit criterion reads "no HS256 in JWKS" literally, and operators correctly read the current output as a regression. This PR closes that gap.

Fix

jwks.ts now gates the HS256 entry on c.env.SIGNING_KEY being bound. Comment rewritten to explain why (publishing k would allow token forgery) and that unbinding SIGNING_KEY is the retirement signal.

Downstream work (not in this PR)

For the production JWKS endpoint to actually drop the HS256 entry:

  1. Republish @relayauth/server (user manages the version bump + publish post-merge).
  2. Bump the @relayauth/server dep in cloud and redeploy the relayauth worker.

Test plan

  • npx tsc --noEmit on packages/server passes
  • New regression tests pass (packages/server/src/__tests__/jwks-rsa.test.ts):
    • JWKS suppresses the HS256 metadata once SIGNING_KEY is unbound (post-sunset) — SIGNING_KEY unset + RSA public PEM set → keys contains only the RSA JWK
    • JWKS returns an empty key set when neither HS256 nor RS256 material is configured — both unset → keys: []
  • Existing JWKS tests still pass (dual-publish during transition window, RSA-only when HS256 unbound, RFC 7638 thumbprint kid)
  • Manual: after republish + cloud redeploy, curl https://<stage>/.well-known/jwks.json returns RSA only

🤖 Generated with Claude Code

The keys array hardcoded the HS256 entry, so deployments that retired
HS256 by removing the SIGNING_KEY binding (per the RS256 migration
step 3 runbook) still advertised HS256 in JWKS. Cosmetic — the entry
never contained `k`, so no verifier could validate HS256 tokens
against it — but it violated the migration exit criteria and confused
operators reading the runbook literally.

Now: HS256 entry only appears when SIGNING_KEY is actually bound.
New regression test covers the post-sunset case (HS256 unbound, RSA
present → only the RSA key) and the empty-config case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit 506f230 into main Apr 23, 2026
2 checks passed
@khaliqgant khaliqgant deleted the fix/jwks-suppress-hs256-when-unconfigured branch April 23, 2026 21:01
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.

2 participants