alishahryar1--free-claude-code
0d5bec3dcd
## Problem Proxy authentication selected the first of three credential headers, so a stale `X-API-Key` could mask valid bearer authorization and leave Claude CLI or IDE clients at their login gate. Fixes #902. ## Changes | Before | After | | --- | --- | | FCC accepted three proxy credential headers and stripped legacy model suffixes. | FCC accepts one exact `Authorization: Bearer` token without mutation. | | Conflicting provider credentials could override valid proxy authorization. | Unrelated credential headers are ignored during proxy authentication. | | Codex catalog discovery and Pi catalog or inference paths used API-key authentication. | Every FCC-owned Codex and Pi path uses bearer authorization. | | Authentication failures referred ambiguously to an API key. | Authentication failures identify the proxy authentication token. | | FCC reported version `4.2.0`. | FCC reports version `4.3.0`. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR makes proxy authentication use bearer tokens only. The main changes are: - Replaced multi-header proxy auth with exact `Authorization: Bearer <token>` checks. - Updated protected route dependencies to use the renamed auth dependency. - Switched FCC-owned Codex and Pi catalog requests to bearer authorization. - Added Pi provider `authHeader` registration for inference requests. - Updated smoke tests, API tests, docs, examples, and package metadata for the new auth contract. </details> <h3>Confidence Score: 4/5</h3> The changed auth flow is mostly consistent, but Codex inference can still fail if its generated provider config sends API-key auth. Server-side bearer parsing is direct and covered by updated tests. Route protection appears preserved after the dependency rename. Codex catalog auth was updated, but the inference path still depends on external client header behavior. The version bump may understate a breaking auth-contract change. src/free_claude_code/api/dependencies.py, src/free_claude_code/cli/launchers/codex.py, pyproject.toml <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - T-Rex executed targeted proxy authentication validation to verify bearer token behavior before and after the change. - T-Rex compared pre-change and post-change test results, confirming 31 passed before and 39 passed after, and validating HTTP 200 for exact bearer with unrelated X-API-Key plus HTTP 401 for missing or invalid tokens. <a href="https://app.greptile.com/trex/runs/14225355/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/free_claude_code/api/dependencies.py | Replaces proxy authentication with exact bearer-token validation and new error details. | | src/free_claude_code/api/routes.py | Updates protected route dependencies to call the renamed auth dependency. | | src/free_claude_code/cli/launchers/codex.py | Changes Codex catalog discovery to send bearer auth while leaving inference auth delegated through Codex config. | | src/free_claude_code/cli/launchers/pi_extension.ts | Changes Pi catalog discovery to bearer auth and registers the provider with `authHeader` enabled. | | smoke/lib/config.py | Updates smoke helper auth headers to emit bearer authorization. | | pyproject.toml | Bumps the package version from 4.2.0 to 4.3.0. | </details> <details open><summary><h3>Sequence Diagram</h3></summary> <a href="#gh-light-mode-only"> ```mermaid %%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Codex participant Launcher as FCC Codex launcher participant API as FCC API Launcher->>API: GET /v1/models with Authorization Bearer token API-->>Launcher: Catalog response Launcher-->>Codex: Raw token in FCC_CODEX_API_KEY Codex->>API: POST /v1/responses with client-built auth alt Client sends bearer authorization API-->>Codex: Accepted else Client sends API-key auth API-->>Codex: 401 proxy auth failure end ``` </a> <a href="#gh-dark-mode-only"> ```mermaid %%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Codex participant Launcher as FCC Codex launcher participant API as FCC API Launcher->>API: GET /v1/models with Authorization Bearer token API-->>Launcher: Catalog response Launcher-->>Codex: Raw token in FCC_CODEX_API_KEY Codex->>API: POST /v1/responses with client-built auth alt Client sends bearer authorization API-->>Codex: Accepted else Client sends API-key auth API-->>Codex: 401 proxy auth failure end ``` </a> </details> <a href="https://app.greptile.com/api/ide/codex?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22alishahryar1%2Ffree-claude-code%22%20on%20the%20existing%20branch%20%22ali%2Fcanonical-proxy-bearer-auth%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22ali%2Fcanonical-proxy-bearer-auth%22.%0A%0AFix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Asrc%2Ffree_claude_code%2Fapi%2Fdependencies.py%3A56%0A**Codex%20Inference%20Still%20Delegates%20Auth**%0A%0AWhen%20Codex%20launches%2C%20FCC%20now%20sends%20bearer%20auth%20only%20for%20its%20own%20%60%2Fv1%2Fmodels%60%20catalog%20request%2C%20but%20inference%20still%20depends%20on%20Codex%20turning%20the%20raw%20%60FCC_CODEX_API_KEY%60%20value%20into%20the%20same%20bearer%20header.%20If%20Codex%20sends%20that%20env%20key%20as%20an%20API-key%20header%2C%20this%20server%20branch%20treats%20the%20request%20as%20missing%20proxy%20auth%20and%20%60%2Fv1%2Fresponses%60%20fails%20with%20401%20even%20though%20catalog%20discovery%20succeeded.%0A%0A%23%23%23%20Issue%202%20of%202%0Apyproject.toml%3A7%0A**Breaking%20Auth%20Contract%20Understated**%0A%0AThis%20change%20removes%20previously%20accepted%20proxy%20credential%20shapes%2C%20including%20%60X-API-Key%60%2C%20%60anthropic-auth-token%60%2C%20and%20suffixed%20bearer%20tokens%2C%20but%20the%20package%20version%20only%20moves%20from%20%604.2.0%60%20to%20%604.3.0%60.%20Existing%20users%20can%20upgrade%20within%20the%20same%20major%20line%20and%20have%20every%20protected%20endpoint%20start%20returning%20401%20until%20their%20clients%20are%20reconfigured%2C%20which%20does%20not%20match%20the%20repository%20guidance%20for%20incompatible%20API%20or%20CLI%20behavior.%0A%0A&repo=alishahryar1%2Ffree-claude-code&pr=1096&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=6"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=6"><img alt="Fix All in Codex" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=6"></picture></a> <sub>Reviews (1): Last reviewed commit: ["fix: make proxy auth bearer-only"](https://github.com/alishahryar1/free-claude-code/commit/c437ee0e23dc8f8411272a8dcfcf1ff69f315857) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=43791345)</sub> > Greptile also left **2 inline comments** on this PR. **Context used:** - Context used - CLAUDE.md ([source](https://app.greptile.com/alishahryar1/github/Alishahryar1/free-claude-code/-/custom-context?memory=d2fd24d8-0dec-4faf-8ee4-e085e215a2f8)) <!-- /greptile_comment -->
150 行
4.6 KiB
Python
150 行
4.6 KiB
Python
from unittest.mock import patch
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from free_claude_code.api.dependencies import get_settings
|
|
from free_claude_code.config.settings import Settings
|
|
from tests.api.support import create_test_app
|
|
|
|
app = create_test_app()
|
|
|
|
|
|
def test_proxy_auth_requires_canonical_bearer_header():
|
|
client = TestClient(app)
|
|
settings = Settings()
|
|
settings.anthropic_auth_token = "s3cr3t"
|
|
app.dependency_overrides[get_settings] = lambda: settings
|
|
|
|
payload = {
|
|
"model": "claude-3-sonnet",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
}
|
|
|
|
with patch("free_claude_code.api.routes.get_token_count", return_value=1):
|
|
r = client.post("/v1/messages/count_tokens", json=payload)
|
|
assert r.status_code == 401
|
|
assert r.json() == {"detail": "Missing proxy authentication token"}
|
|
assert r.headers["request-id"].startswith("req_")
|
|
assert "x-should-retry" not in r.headers
|
|
|
|
for headers in (
|
|
{"X-API-Key": "s3cr3t"},
|
|
{"anthropic-auth-token": "s3cr3t"},
|
|
):
|
|
r = client.post(
|
|
"/v1/messages/count_tokens",
|
|
json=payload,
|
|
headers=headers,
|
|
)
|
|
assert r.status_code == 401
|
|
assert r.json() == {"detail": "Missing proxy authentication token"}
|
|
|
|
r = client.post(
|
|
"/v1/messages/count_tokens",
|
|
json=payload,
|
|
headers={"Authorization": "Bearer s3cr3t"},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["input_tokens"] == 1
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_proxy_auth_ignores_conflicting_legacy_headers():
|
|
client = TestClient(app)
|
|
settings = Settings()
|
|
settings.anthropic_auth_token = "b3artoken"
|
|
app.dependency_overrides[get_settings] = lambda: settings
|
|
|
|
payload = {
|
|
"model": "claude-3-sonnet",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
}
|
|
|
|
with patch("free_claude_code.api.routes.get_token_count", return_value=2):
|
|
r = client.post(
|
|
"/v1/messages/count_tokens",
|
|
json=payload,
|
|
headers={
|
|
"Authorization": "Bearer b3artoken",
|
|
"X-API-Key": "stale-anthropic-key",
|
|
"anthropic-auth-token": "stale-proxy-token",
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["input_tokens"] == 2
|
|
|
|
r = client.post(
|
|
"/v1/messages/count_tokens",
|
|
json=payload,
|
|
headers={
|
|
"Authorization": "Bearer wrong",
|
|
"X-API-Key": "b3artoken",
|
|
},
|
|
)
|
|
assert r.status_code == 401
|
|
assert r.json() == {"detail": "Invalid proxy authentication token"}
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_anthropic_auth_token_normalizes_configured_whitespace():
|
|
client = TestClient(app)
|
|
settings = Settings()
|
|
settings.anthropic_auth_token = " spaced-token \n"
|
|
app.dependency_overrides[get_settings] = lambda: settings
|
|
|
|
payload = {
|
|
"model": "claude-3-sonnet",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
}
|
|
|
|
with patch("free_claude_code.api.routes.get_token_count", return_value=3):
|
|
r = client.post(
|
|
"/v1/messages/count_tokens",
|
|
json=payload,
|
|
headers={"Authorization": "Bearer spaced-token"},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["input_tokens"] == 3
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_anthropic_auth_token_applies_to_models_endpoint():
|
|
client = TestClient(app)
|
|
settings = Settings()
|
|
settings.anthropic_auth_token = "models-token"
|
|
app.dependency_overrides[get_settings] = lambda: settings
|
|
|
|
r = client.get("/v1/models")
|
|
assert r.status_code == 401
|
|
assert r.headers["x-request-id"] == r.headers["request-id"]
|
|
assert "x-should-retry" not in r.headers
|
|
|
|
r = client.get("/v1/models", headers={"Authorization": "Bearer models-token"})
|
|
assert r.status_code == 200
|
|
assert "data" in r.json()
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_root_get_requires_auth_but_root_probes_are_public():
|
|
client = TestClient(app)
|
|
settings = Settings()
|
|
settings.anthropic_auth_token = "root-token"
|
|
app.dependency_overrides[get_settings] = lambda: settings
|
|
|
|
response = client.get("/")
|
|
assert response.status_code == 401
|
|
|
|
head = client.head("/")
|
|
assert head.status_code == 204
|
|
assert head.headers["Allow"] == "GET, HEAD, OPTIONS"
|
|
|
|
options = client.options("/")
|
|
assert options.status_code == 204
|
|
assert options.headers["Allow"] == "GET, HEAD, OPTIONS"
|
|
|
|
app.dependency_overrides.clear()
|