Skip to content

fix(auth): accept valid x-api-key even when an Authorization header is present (Claude Code) - #2926

Closed
omar-nahhas wants to merge 1 commit into
decolua:masterfrom
omar-nahhas:fix/anthropic-multi-credential-auth
Closed

omar-nahhas wants to merge 1 commit into
decolua:masterfrom
omar-nahhas:fix/anthropic-multi-credential-auth

Conversation

@omar-nahhas

Copy link
Copy Markdown
Contributor

Problem

Anthropic clients can send an Authorization: Bearer … header alongside x-api-key. Claude Code does this whenever the user has an active claude.ai session or ANTHROPIC_AUTH_TOKEN set while ANTHROPIC_API_KEY points at 9router. The real Anthropic API authenticates on x-api-key regardless of other headers — but 9router's extractApiKey (both in dashboardGuard.js and src/sse/services/auth.js) picks the Authorization bearer first and never falls back, so a valid client API key gets 401 whenever any bearer is present.

Reproduction against a 9router instance with requireApiKey enabled:

# works
curl -X POST $BASE/v1/messages -H 'x-api-key: <valid-9router-key>' ...
# 401 — same valid key, plus the stale bearer Claude Code sends
curl -X POST $BASE/v1/messages \
  -H 'Authorization: Bearer sk-ant-oat01-…' \
  -H 'x-api-key: <valid-9router-key>' ...

Fix

  • The guard and the SSE auth service now collect all presented credentials (Authorization bearer, x-api-key, x-goog-api-key, ?key=) in precedence order and accept the request if any of them validates.
  • New resolveClientApiKey() returns the credential that actually validated, so per-key usage attribution records the real key instead of a stale bearer.
  • All /v1 handlers (chat/messages, embeddings, search, fetch, stt, tts, image, video) and the Gemini-native /v1beta route use the multi-credential check.
  • 401s on /v1/messages now return the standard Anthropic error envelope ({"type":"error","error":{"type":"authentication_error",…}}) so Claude Code and anthropic SDKs surface the message instead of an opaque body. Other endpoints keep their existing error shape.

Tests

  • New unit tests cover the both-headers case, candidate collection order, and the Anthropic 401 envelope (tests/unit/dashboard-guard.test.js).
  • unit/dashboard-guard, unit/embeddingsCore, unit/image-generation, unit/gemini-native-endpoint, unit/gemini-tts, unit/xai-video-handler, unit/minimax-tts, unit/kiro-nonstream-error: 112/112 passing on this branch.

🤖 Generated with Claude Code

…nts)

Claude Code with an active claude.ai session (or ANTHROPIC_AUTH_TOKEN set)
sends an Authorization bearer ALONGSIDE the x-api-key. Both the edge guard
and the SSE handlers picked the Authorization header first and never fell
back, so a valid provisioned key 401'd whenever any bearer was present —
api.anthropic.com authenticates on x-api-key regardless.

- guard + auth service now validate all presented credentials in order
- new resolveClientApiKey() returns the credential that actually validated,
  so per-key usage attribution records the real key, not a stale bearer
- all /v1 handlers (chat, embeddings, search, fetch, stt, tts, image, video)
  and the Gemini-native route switched to the multi-credential check
- 401s on /v1/messages now use the standard Anthropic error envelope
  ({type:"error",error:{type:"authentication_error",...}}) so anthropic
  SDKs/Claude Code surface the message instead of an opaque body

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omar-nahhas

Copy link
Copy Markdown
Contributor Author

Superseded by #5091 (rebased onto current master).

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