Skip to content

feat(creds): inject Claude subscription OAuth at the proxy edge#21

Open
trevor-vaughan wants to merge 1 commit into
bbrowning:mainfrom
trevor-vaughan:feat/anthropic-oauth-injector
Open

feat(creds): inject Claude subscription OAuth at the proxy edge#21
trevor-vaughan wants to merge 1 commit into
bbrowning:mainfrom
trevor-vaughan:feat/anthropic-oauth-injector

Conversation

@trevor-vaughan
Copy link
Copy Markdown

Let a containerized Claude Code session use a Claude subscription (OAuth) without the real token ever reaching the agent, the same edge-injection model already used for gcloud ADC.

  • Add AnthropicOAuthInjector: always overrides Authorization: Bearer for api.anthropic.com and .claude.ai from a creds file mounted into the proxy.
  • Piggyback Claude Code's own OAuth refresh inside the proxy instead of refreshing on a timer: rewrite the agent's refresh request with the real token, capture the rotated tokens upstream, hand the agent a dummy response.
  • Wire a new "anthropic_oauth" injector type via ANTHROPIC_OAUTH_CREDS_FILE, inert when the var is unset (same as the gcloud entry).
  • Net effect: the agent only ever holds dummy bearer tokens; the real credential lives solely in the proxy.

Details:

Injector (internal/credentials/anthropic_oauth.go):

  • Lazy-loads the creds file (~/.claude/.credentials.json shape) under a mutex; the injector itself never calls the OAuth endpoint.
  • CurrentRefreshToken exposes the real refresh token to the intercept layer.
  • UpdateFromRefresh records rotated tokens and writes them back atomically (temp file + rename, 0600), preserving scopes and subscriptionType. Keeps the existing refresh token when the response omits a rotated one.

Refresh piggyback (internal/proxy/proxy.go, oauth_refresh.go):

  • Request hook rewrites only genuine grant_type=refresh_token bodies. An authorization_code grant or invalid JSON passes through untouched and is not marked for response interception, which closes a bug where a non-refresh 200 body got replaced with a dummy.
  • Response hook captures the rotated tokens (when access_token is non-empty) and swaps in a dummy body that mirrors expires_in so the agent's local expiry stays in sync. Non-200 responses pass through so the agent sees the real failure.

Token vending (internal/credentials/token_vending.go):

  • Add IsAnthropicTokenExchange for console.anthropic.com and platform.claude.com (/v1/oauth/token, /api/oauth/token), matching host with or without the :443 suffix. Correct two misleading comments.

Logging (internal/proxy/redact.go):

  • logRefreshDiag is the single named site for refresh logging. It records host and path only, never token material, bodies, or Authorization headers.

Config wiring (config.go, credentials.json, main.go):

  • BuildFromConfig returns the injector as a 4th value; main threads it into proxy.Config.AnthropicInjector to activate the refresh hooks in production.
  • Default routing table gains the ANTHROPIC_OAUTH_CREDS_FILE entry pointing at api.anthropic.com and .claude.ai.

Tests:

  • Cover injection/override, rotation with field preservation, the refresh-only-on-refresh-grant contract, a guard that no token string ever reaches the log, config wiring (non-nil with creds, nil without), and an end-to-end refresh integration.

Security note: the injected token is scoped to Anthropic endpoints and is meant to serve Claude Code over this transport only, not for independent token use.

Assisted-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Let a containerized Claude Code session use a Claude subscription (OAuth)
without the real token ever reaching the agent, the same edge-injection model
already used for gcloud ADC.

- Add AnthropicOAuthInjector: always overrides Authorization: Bearer for
  api.anthropic.com and .claude.ai from a creds file mounted into the proxy.
- Piggyback Claude Code's own OAuth refresh inside the proxy instead of
  refreshing on a timer: rewrite the agent's refresh request with the real
  token, capture the rotated tokens upstream, hand the agent a dummy response.
- Wire a new "anthropic_oauth" injector type via ANTHROPIC_OAUTH_CREDS_FILE,
  inert when the var is unset (same as the gcloud entry).
- Net effect: the agent only ever holds dummy bearer tokens; the real
  credential lives solely in the proxy.

Details:

Injector (internal/credentials/anthropic_oauth.go):
- Lazy-loads the creds file (~/.claude/.credentials.json shape) under a mutex;
  the injector itself never calls the OAuth endpoint.
- CurrentRefreshToken exposes the real refresh token to the intercept layer.
- UpdateFromRefresh records rotated tokens and writes them back atomically
  (temp file + rename, 0600), preserving scopes and subscriptionType. Keeps
  the existing refresh token when the response omits a rotated one.

Refresh piggyback (internal/proxy/proxy.go, oauth_refresh.go):
- Request hook rewrites only genuine grant_type=refresh_token bodies. An
  authorization_code grant or invalid JSON passes through untouched and is not
  marked for response interception, which closes a bug where a non-refresh 200
  body got replaced with a dummy.
- Response hook captures the rotated tokens (when access_token is non-empty)
  and swaps in a dummy body that mirrors expires_in so the agent's local
  expiry stays in sync. Non-200 responses pass through so the agent sees the
  real failure.

Token vending (internal/credentials/token_vending.go):
- Add IsAnthropicTokenExchange for console.anthropic.com and
  platform.claude.com (/v1/oauth/token, /api/oauth/token), matching host with
  or without the :443 suffix. Correct two misleading comments.

Logging (internal/proxy/redact.go):
- logRefreshDiag is the single named site for refresh logging. It records host
  and path only, never token material, bodies, or Authorization headers.

Config wiring (config.go, credentials.json, main.go):
- BuildFromConfig returns the injector as a 4th value; main threads it into
  proxy.Config.AnthropicInjector to activate the refresh hooks in production.
- Default routing table gains the ANTHROPIC_OAUTH_CREDS_FILE entry pointing at
  api.anthropic.com and .claude.ai.

Tests:
- Cover injection/override, rotation with field preservation, the
  refresh-only-on-refresh-grant contract, a guard that no token string ever
  reaches the log, config wiring (non-nil with creds, nil without), and an
  end-to-end refresh integration.

Security note: the injected token is scoped to Anthropic endpoints and is meant
to serve Claude Code over this transport only, not for independent token use.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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