Skip to content

Auth: read the Authorization scheme in any case (BASIC is a credential) - #864

Merged
schmitch merged 1 commit into
mainfrom
claude/584-auth-scheme-case-insensitive
Nov 3, 2026
Merged

schmitch merged 1 commit into
mainfrom
claude/584-auth-scheme-case-insensitive

Conversation

@schmitch

@schmitch schmitch commented Nov 3, 2026

Copy link
Copy Markdown
Member

Fixes #584

What

Authorization schemes are case-insensitive (RFC 9110 §11.1). The Basic source in crates/core/src/auth/token.rs read only Basic and basic , so Authorization: BASIC <credentials> was no credential to the middleware. Behind a trusted-header realm's proxy the headers then named the caller instead of the request's own password being checked. A wrong password ran as the network's account instead of getting a 401.

How

  • New forge_core::auth::scheme with one helper. credentials(value, scheme) returns the trimmed credentials when the scheme matches in any case. header_credentials(headers, scheme) reads them from the request and returns None when they are empty.

  • The Basic source (parse_basic and its scheme check) now uses it.

  • The other readers switch from their own copies to it. These were already case-insensitive, so their behaviour does not change:

    • the workload-identity source (wif.rs, whose local strip_prefix_ci is removed)
    • the token exchange's bearer_token
    • the npm Bearer token on feeds (plugins.rs)
    • the registry's bearer_token (images/dispatch.rs)
  • I grepped all of crates/, including crates/formats/*, for the other "Basic " / "Bearer " matches. None reads a request's credentials:

    • the WWW-Authenticate challenges Forge sends;
    • the headers Forge builds for upstream calls;
    • the upstream challenge parser in images/proxy.rs, already case-insensitive;
    • test mocks that parse headers Forge itself sends (crates/e2e/src/mock_basic_upstream.rs, crates/server/tests/client_passwords.rs).

    I left all of these unchanged.

Tests

  • auth::scheme unit tests: Basic/basic/BASIC/bAsIc and Bearer/BEARER/bEaReR are read; another scheme or a scheme alone is not.
  • token.rs: parse_basic reads BASIC/bAsIc. BasicAuthSource treats BASIC …/bAsIc … as its own credential, so an unreadable one is Invalid and never NoCredentials.
  • images/dispatch.rs: bearer_token reads bearer/BEARER/bEaReR.
  • crates/server/tests/trusted_header.rs, basic_in_another_case_is_the_requests_own_credential: with a trusted-header realm, BASIC/bAsIc with a wrong password gets 401 invalid credentials, not the account the headers name. With the right password it runs as the request's own account. Against the old token.rs this test fails: BASIC <wrong password> returned 200 as the headers' account.

Checks run locally:

  • cargo fmt --all --check
  • cargo clippy -p forge-core -p forge-server --all-targets -- -D warnings
  • cargo test -p forge-core --lib auth::: 112 passed
  • cargo test -p forge-server --lib on the auth, dispatch, plugins and principal tests: 90 passed
  • cargo test -p forge-server --test trusted_header: 12 passed
  • cargo xtask changelog --check

Changelog

docs/changes/2.2.0/584.fixed.md. I filed it as fixed rather than security. The trusted-header case gives the caller only the account the proxy already vouched for, not more access. The bug is that the request's own wrong password is not refused.


Generated by Claude Code

…source

Authorization schemes are case-insensitive (RFC 9110 §11.1), but the Basic
source read only `Basic ` and `basic `: `Authorization: BASIC <credentials>`
was no credential to the middleware, so behind a trusted-header realm's
proxy the headers named the caller instead of the request's own password
being checked.

Add forge_core::auth::scheme, one helper that reads an Authorization
value's credentials for a scheme in any case, and use it in the Basic
source, the workload-identity source, the token exchange, the npm Bearer
token on feeds and the registry's Bearer token, instead of their copies.

Fixes #584

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJbqjKPPeknBMf5kEFg
@schmitch
schmitch added this pull request to the merge queue Nov 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 3, 2026

schmitch commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

The merge queue dropped this PR on a test failure that isn't this PR's.

Check: merge-group run 41711313673, job test. browse::a_proxy_package_page_answers_from_the_cache_and_refreshes_off_the_request failed at crates/server/tests/browse.rs:1278 (a refresh of a fresh page, left 7, right 6). Every other test binary passed, trusted_header among them.

Why it isn't this PR's: the test and the code it covers (crates/server/src/browse.rs, BackgroundRefreshes) came in with #841 (3e0d5ca) today. This PR doesn't touch either, and the test's requests carry no Authorization scheme other than Basic. Locally the test passed 8 of 8 runs on main (8c05683) and 8 of 8 on this PR merged with main.

Cause (a race): package_detail reports refreshing only for the proxies in listing.due. refresh_from_upstream renews the listing first and then fetches the registration, the tags and the latest manifest. Once the listing is fresh, the page already answers without refreshing while that tail is still asking the upstream. The test takes done = upstream.total() at that moment, and the last fetch lands within its 200 ms.

Fix: none exists yet. I've left it out of this PR to keep it to #584. A fix would have BackgroundRefreshes report refreshing while any refresh of the package runs (an entry seen[(proxy, id)] == None), not only for the proxies whose listing is due. That fixes the flag the UI shows as well as the test.

This PR needs re-queuing; it's green on its head.


Generated by Claude Code

@schmitch
schmitch added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 787b42b Nov 3, 2026
19 checks passed
@schmitch
schmitch deleted the claude/584-auth-scheme-case-insensitive branch November 3, 2026 20:53
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.

Auth: an Authorization scheme in other case (BASIC) is no credential, and a trusted-header realm then names the caller

2 participants