Skip to content

Auth realms: accept client passwords over HTTP Basic per realm - #708

Merged
schmitch merged 3 commits into
mainfrom
schmitch/realm-client-passwords-647
Oct 30, 2026
Merged

schmitch merged 3 commits into
mainfrom
schmitch/realm-client-passwords-647

Conversation

@schmitch

Copy link
Copy Markdown
Member

Fixes #647

What was wrong

Whether a client could sign in with a password was one install-wide setting, allow_password_basic (off by default). An enabled credential-forwarding realm turned it on by itself for the whole chain (RealmSnapshot::forwards_credentials). So enabling the Nexus realm made every local password work from every client, whatever the setting said. The npm legacy login was not gated at all.

The change

  • A per-realm flag, accept_client_passwords, on the two password realms:
    • credential_forwarding: on by default, including the realm seeded from [auth.upstream_basic];
    • local: off by default (new LocalRealmConfig, which refuses unknown keys like the other kinds).
  • Two chains per realm snapshot (crates/server/src/auth/realms.rs):
    • chain: every password realm, for the browser login (POST /auth/login, and the /cli-login page behind it), unchanged;
    • client_chain: the realms whose flag is on. It serves HTTP Basic on the feeds and /v2/token (impl ChainSource for RealmAuth) and the npm legacy login (verify_password_login(…, PasswordFrom::Client)). It is None, so the path is off, when no realm accepts client passwords.
    • Both chains share one provider per forwarding realm (impl AuthProvider for Arc<P>), so there is one HTTP client and one verdict cache per realm.
  • A closed local realm still guards its names. It stays in the client chain as LocalPasswordProvider::reject_only() (crates/core/src/auth/local_password.rs):
    • a name the local store owns is refused after the dummy argon2 verification, so it answers with the 401 of a wrong password, timing included, and the lockout counts it;
    • only a foreign name goes on to the forwarding realm.
  • Unchanged: an API token as the Basic password always works, and the admin API never takes a password.

Upgrade

  • Migration (migrations/0006_realm_client_passwords.sql): the local realm takes the value of allow_password_basic from the stored settings (off when absent).
  • Forwarding realms' stored config is left alone, since their default is on. A realm seeded from [auth.upstream_basic] therefore keeps matching its seed fingerprint and keeps re-applying.
  • The old key in the settings document: the lenient read ignores it, and the next save drops it. The setting leaves AuthSettings.
  • The old key in a request: PUT /admin/settings/auth refuses allow_password_basic with a 400 that says where it went. A leftover auth.allow_password_basic in the config file was already refused at startup; that message now names the realm flag.

Admin API and web UI

  • /admin/realms takes and shows the flag in the realm's config.
  • Admin → Authentication gets a Clients panel with Accept passwords from clients (HTTP Basic) on the local realm and on credential-forwarding realms. Its text says that users of a closed realm sign their clients in with tokens. The edit screen now sends the local realm's config too.
  • Admin → Settings loses the checkbox and gains a sentence pointing to Authentication.

Harnesses and docs

  • Harnesses: the server test harness, the e2e stack, the Playwright setup and cargo xtask seed set the local realm's flag instead of the setting. A new TestServer::set_local_client_passwords helper does it in tests.
  • Docs:
    • docs/deployment.md: Password Basic auth becomes Passwords from clients, and the settings table loses the row;
    • docs/docker.md and docs/npm.md describe the per-realm flag;
    • DEVELOPMENT.md and README.md are updated to match.
  • Changelog: 647.feature.md and 647.upgrade.md.

Tests

  • crates/server/tests/client_passwords.rs (new):
    • The issue's case. A forwarding realm over a wiremock upstream and a closed local realm. The forwarded user's NuGet and raw Basic reads, docker login (/v2/token) and npm login --auth-type=legacy work. The local admin's password is refused on each, with exactly the answer of a wrong password (status and body). The upstream received no request naming the local user. The admin's API token works on the feeds, docker login and npm. The browser login takes both users' passwords.
    • Toggling each realm's flag through the admin API applies at once. A closed forwarding realm forwards no client's password, though its users still sign in through the browser.
    • The admin API shows the defaults (local off, forwarding on) and refuses a non-boolean flag and an unknown local key.
    • The upgrade: the migration on a database with allow_password_basic true, false, absent, and no settings row. The forwarding realm's config is unchanged, and the settings document still loads.
  • crates/core/tests/local_password.rs: reject_only refuses an active local account's right password, and skips an unknown name, with and without forwarding after it.
  • Updated tests:
    • auth_settings.rs: the settings no longer carry the key, and a PUT naming it gets a 400;
    • service_accounts.rs uses the new helper;
    • a realm unit test covers the local config.
  • Web:
    • realms.test.tsx: the local realm's checkbox, off and sent when checked, and a forwarding realm's, on and sent as false when unchecked;
    • RealmForm.test.ts: round trips;
    • settings.test.tsx: the checkbox is gone.

Checks run

  • forge-core: cargo test -p forge-core --lib --test local_password --test upstream_basic.
  • forge-server: cargo test -p forge-server --lib and --test for client_passwords, auth_settings, service_accounts, auth_realms, realm_seed, credential_kinds, invitations, release_hardening, trusted_header, npm, registry, raw, admin, admin_rbac, me_tokens, telemetry. All passed. raw and registry needed RUST_MIN_STACK=16777216 in this session: raw's groups_fall_through_their_members_by_priority overflows the default test stack here on main too, with the full debug info this session builds with. CI builds with line tables only.
  • Lint and repo checks: cargo clippy -p forge-core -p forge-server -p xtask --all-targets -- -D warnings, cargo clippy -p forge-e2e --all-targets --features forge-e2e -- -D warnings, cargo test -p xtask, cargo fmt --all --check, cargo xtask changelog --check, cargo xtask docs --check.
  • Web: pnpm run tsc, pnpm run e2e:tsc, pnpm run test (527 passed).
  • Not run here: the e2e suite (forge-e2e) and Playwright; CI runs them.

Generated by Claude Code

Whether a client may sign in with a password (HTTP Basic on the feeds and
/v2/token, npm login --auth-type=legacy) was one install-wide setting,
allow_password_basic, and an enabled credential-forwarding realm turned it
on for the whole chain: every local password then worked from every
client, whatever the setting said.

It is now each password realm's own accept_client_passwords: on by default
for credential_forwarding realms (the Nexus case), off for the local realm.
The realm snapshot builds a second chain for clients from the realms that
accept them. A local realm that does not stays in it reject-only
(LocalPasswordProvider::reject_only): a password a client sends for a local
account is refused like a wrong one and never forwarded upstream. With no
realm accepting client passwords the path is off. The browser login keeps
the full chain, and an API token works as before.

The upgrade (0006) copies allow_password_basic onto the local realm; the
setting leaves the settings document and Admin -> Settings, and a PUT that
still names it is refused with where it went. The realm form gets the
checkbox on the local and credential-forwarding realms. The test
harnesses, the Playwright setup and cargo xtask seed set the local realm's
flag instead.

Fixes #647

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJXRhLYxFjzrWiwKFKc
Comment thread crates/server/src/auth/realms.rs Fixed
CodeQL's cleartext-logging query took the realm flag for a secret by its
name: the tuple holding the local provider's position and
`accept_client_passwords` was tainted, and the position reached
`Vec::insert`, whose panic message goes to stderr. The flag is a yes or
no, not a password. Its Rust field is `accepts_clients` now, still
`accept_client_passwords` in the realm's config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJXRhLYxFjzrWiwKFKc
Yarn's legacy login sends the admin's local password, and the local realm
now takes passwords from clients only once it accepts them. The step turns
that on through the realm's config, as the Rust and web e2e harnesses do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJXRhLYxFjzrWiwKFKc
@schmitch
schmitch added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 882932a Oct 30, 2026
19 checks passed
@schmitch
schmitch deleted the schmitch/realm-client-passwords-647 branch October 30, 2026 09:29
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: accept client passwords over HTTP Basic per realm, on for credential forwarding (Nexus), off otherwise

3 participants