项目文件夹

文件
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

203 行
6.1 KiB
Python

from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException, Request
from free_claude_code.api.dependencies import (
get_services,
get_settings,
require_proxy_auth,
resolve_provider,
)
from free_claude_code.api.ports import ApiServices
from free_claude_code.application.errors import ApplicationUnavailableError
from free_claude_code.application.ports import RequestRuntimeLease
from free_claude_code.config.settings import Settings
from tests.api.support import create_test_app
def _request(*, headers: dict[str, str], token: str) -> tuple[Request, Settings]:
request = Request(
{
"type": "http",
"method": "GET",
"path": "/",
"headers": [
(key.lower().encode(), value.encode()) for key, value in headers.items()
],
}
)
settings = Settings.model_construct(anthropic_auth_token=token)
return request, settings
def _lease(*, provider=None, error: Exception | None = None):
lease = MagicMock(spec=RequestRuntimeLease)
lease.is_provider_cached.return_value = False
if error is None:
lease.resolve_provider.return_value = provider or MagicMock()
else:
lease.resolve_provider.side_effect = error
return lease
def test_get_services_reads_the_single_app_state_boundary() -> None:
app = create_test_app()
request = Request({"type": "http", "app": app})
services = get_services(request)
assert services is app.state.services
assert isinstance(services, ApiServices)
def test_get_settings_reads_current_request_runtime_settings() -> None:
app = create_test_app(
Settings.model_construct(
model="deepseek/test-model",
anthropic_auth_token="",
)
)
assert get_settings(app.state.services).model == "deepseek/test-model"
def test_resolve_provider_uses_retained_lease_and_logs_first_initialization() -> None:
provider = MagicMock()
lease = _lease(provider=provider)
with patch("free_claude_code.api.dependencies.logger.info") as log_info:
result = resolve_provider("nvidia_nim", lease=lease)
assert result is provider
lease.resolve_provider.assert_called_once_with("nvidia_nim")
log_info.assert_called_once_with("Provider initialized: {}", "nvidia_nim")
def test_resolve_provider_skips_initialization_log_for_cached_provider() -> None:
lease = _lease()
lease.is_provider_cached.return_value = True
with patch("free_claude_code.api.dependencies.logger.info") as log_info:
resolve_provider("nvidia_nim", lease=lease)
log_info.assert_not_called()
def test_resolve_provider_missing_key_preserves_readiness_error() -> None:
lease = _lease(
error=ApplicationUnavailableError(
"OPENROUTER_API_KEY is required. Get one at https://openrouter.ai"
)
)
with pytest.raises(ApplicationUnavailableError) as exc_info:
resolve_provider("open_router", lease=lease)
assert exc_info.value.status_code == 503
assert "OPENROUTER_API_KEY" in exc_info.value.message
assert "openrouter.ai" in exc_info.value.message
def test_resolve_provider_unrelated_error_is_not_reclassified() -> None:
lease = _lease(error=ValueError("unrelated config"))
with pytest.raises(ValueError, match="unrelated config"):
resolve_provider("nvidia_nim", lease=lease)
def test_require_proxy_auth_allows_when_no_token_configured():
request, settings = _request(headers={}, token="")
require_proxy_auth(request, settings)
def test_require_proxy_auth_rejects_missing_authorization():
request, settings = _request(headers={}, token="secret")
with pytest.raises(HTTPException) as exc_info:
require_proxy_auth(request, settings)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Missing proxy authentication token"
@pytest.mark.parametrize("header_name", ["x-api-key", "anthropic-auth-token"])
def test_require_proxy_auth_rejects_legacy_header_only(header_name: str):
request, settings = _request(headers={header_name: "secret"}, token="secret")
with pytest.raises(HTTPException) as exc_info:
require_proxy_auth(request, settings)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Missing proxy authentication token"
def test_require_proxy_auth_accepts_exact_bearer_token():
request, settings = _request(
headers={"authorization": "bEaReR secret"},
token="secret",
)
require_proxy_auth(request, settings)
def test_require_proxy_auth_accepts_colons_in_configured_token():
request, settings = _request(
headers={"authorization": "Bearer secret:with:colons"},
token="secret:with:colons",
)
require_proxy_auth(request, settings)
def test_require_proxy_auth_accepts_valid_bearer_with_conflicting_legacy_headers():
request, settings = _request(
headers={
"authorization": "Bearer secret",
"x-api-key": "wrong",
"anthropic-auth-token": "also-wrong",
},
token="secret",
)
require_proxy_auth(request, settings)
@pytest.mark.parametrize(
"authorization",
[
"secret",
"Basic secret",
"Bearer",
"Bearer wrong",
"Bearer secret:claude-sonnet",
],
)
def test_require_proxy_auth_rejects_malformed_or_invalid_authorization(
authorization: str,
):
request, settings = _request(
headers={"authorization": authorization},
token="secret",
)
with pytest.raises(HTTPException) as exc_info:
require_proxy_auth(request, settings)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Invalid proxy authentication token"
def test_require_proxy_auth_rejects_invalid_bearer_when_legacy_header_matches():
request, settings = _request(
headers={"authorization": "Bearer wrong", "x-api-key": "secret"},
token="secret",
)
with pytest.raises(HTTPException) as exc_info:
require_proxy_auth(request, settings)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Invalid proxy authentication token"