Skip to content

Client passwords: a forwarding realm that accepts none guards its own accounts - #882

Merged
schmitch merged 1 commit into
mainfrom
claude/756-forwarding-realm-reject-guard
Nov 3, 2026
Merged

schmitch merged 1 commit into
mainfrom
claude/756-forwarding-realm-reject-guard

Conversation

@schmitch

@schmitch schmitch commented Nov 3, 2026

Copy link
Copy Markdown
Member

Fixes #756

What changed

A credential-forwarding realm with accept_client_passwords: false was dropped from the clients' password chain (HTTP Basic on the feeds and /v2/token, npm login --auth-type=legacy). Its users' passwords therefore went on to the upstream of a credential-forwarding realm after it. The sign-in was still refused, but the password had already reached a server that was not the user's realm's.

  • ForwardingRealmGuard (new, crates/core/src/auth/realm_guard.rs): takes the closed realm's place in the client chain. It asks no upstream. For a name the realm owns (find_upstream_user(username, Some(realm_id)), active or not) it returns AuthOutcome::NotAccepted. Any other name gets Skip, and a failed lookup gets Failed, so the password is not forwarded on a database error either.
  • Chain: AuthOutcome::NotAccepted now ends the chain whatever provider returns it. Before, only a Local provider's did; it was introduced in Client passwords: a closed local realm's refusal no longer counts towards the login lockout #845 for the local realm's reject-only guard. The outcome stays ChainOutcome::NotAccepted: the same 401 as a wrong password, and not counted towards the sign-in lockout, since nothing was checked.
  • RealmCache::build (crates/server/src/auth/realms.rs): the client chain keeps every credential-forwarding realm in its place. An accepting realm keeps its provider and a closed one gets its guard. The local provider's forwarding_follows now looks only at providers that actually ask an upstream, so a local realm followed only by guards still pays its dummy hash. The browser-login chain is unchanged.
  • Docs: docs/deployment.md (Passwords from clients) gets a bullet for the closed forwarding realm. Doc comments in provider.rs, chain.rs and token.rs are updated.

Tests

  • crates/server/tests/client_passwords.rs covers the issue's scenario. Realm order is local, B (corp: forwarding, closed, own mock upstream), then A (nexus: forwarding, accepting).
    • a_closed_forwarding_realms_passwords_never_reach_the_next_realms_upstream: B's user signs NuGet, raw, docker login and npm login in with B's right password and with a wrong one. Every client gets 401, both passwords get the same answers, and neither A's upstream nor B's receives anything. A's own user still gets past B's guard to A. The browser login still accepts B's password, checked at B's upstream only.
    • a_closed_forwarding_realm_guards_its_deactivated_accounts: the same guard holds for a deactivated B account.
    • Without the fix both tests fail: with the source changes stashed and the tests kept, A's upstream received B's user's password (left: 2, right: 0 and left: 1, right: 0).
  • crates/core/src/auth/chain.rs unit test an_upstream_realms_not_accepted_is_terminal_and_never_forwarded: a non-local NotAccepted stops the chain, and the next upstream is never consulted.
  • Run locally, all green: cargo fmt --all --check, cargo clippy -p forge-core -p forge-server --all-targets -- -D warnings, cargo test -p forge-core, cargo test -p forge-server --lib plus the admin, auth_realms, auth_settings, client_passwords, credential_kinds, invitations and realm_seed integration tests, and cargo xtask changelog --check.

Changelog

docs/changes/2.2.0/756.security.md (Security): the leak, and that a closed forwarding realm now keeps its accounts' names in the clients' chain.

Notes

  • The guard only knows accounts the realm has already created. A B user who has never signed in has no account, so their client password still goes on to the next realm. Forge cannot tell that name is B's without asking B's upstream, which a closed realm must not do. The deployment docs say so.

Not fixed here


Generated by Claude Code

… accounts

A credential-forwarding realm with `accept_client_passwords: false` was left
out of the clients' password chain, so its users' passwords went on to the
upstream of a credential-forwarding realm after it. It now keeps its place
there as a reject-only guard (`ForwardingRealmGuard`): a name of one of its
accounts (`find_upstream_user(username, realm_id)`) is refused as
`NotAccepted`, which now ends the chain from any provider, so the password
reaches no upstream. Any other name goes on.

Fixes #756

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJawoR76E4QRqqppNmL
@schmitch
schmitch enabled auto-merge November 3, 2026 22:35
@schmitch
schmitch disabled auto-merge November 3, 2026 22:48
@schmitch
schmitch added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 1ea5d65 Nov 3, 2026
19 checks passed
@schmitch
schmitch deleted the claude/756-forwarding-realm-reject-guard branch November 3, 2026 22:57
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 realms: a credential-forwarding realm that accepts no client passwords lets its users' passwords go to the next realm's upstream

2 participants