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 -->
203 行
6.1 KiB
Python
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"
|