项目文件夹

文件
Ali Khokhar 0d5bec3dcd Make proxy authentication bearer-only across every client (#1096)
## 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 -->
2026-07-13 04:26:20 -07:00

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()