Skip to content

Store native credentials as JSON by authentication realm - #581

Draft
ewdurbin wants to merge 31 commits into
zaniebot/auth-prefix-precedencefrom
native-auth-as-json
Draft

ewdurbin wants to merge 31 commits into
zaniebot/auth-prefix-precedencefrom
native-auth-as-json

Conversation

@ewdurbin

@ewdurbin ewdurbin commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

Store native credentials for a realm in a JSON entry that retains each account’s service URL. Share realm snapshots across requests and migrate legacy entries into this representation, so credentials remain scoped to the matching service and account.

Depends on #2591 for authentication URL prefix precedence.

@astral-automations-bot astral-automations-bot Bot added enhancement New feature or request preview Experimental behavior labels Aug 6, 2026
@zaniebot zaniebot added priority:3 Normal: useful features, documentation, usability, or routine efficiency improvements. size:5 Project-sized: too much semantic scope for a normal review; needs decomposition or design review. risk:3 Credible risk of meaningful incorrect behavior or regression; bounded or recoverable. labels Sep 9, 2026
@astral-automations-bot
astral-automations-bot Bot force-pushed the native-auth-as-json branch 2 times, most recently from 841dc59 to 53e2890 Compare September 11, 2026 20:12
@codspeed

codspeed Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 8 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 54 untouched benchmarks
⏩ 35 skipped benchmarks1


Comparing native-auth-as-json (5d65ecd) with zaniebot/auth-prefix-precedence (fdef614)

Open in CodSpeed

Footnotes

  1. 35 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@astral-automations-bot
astral-automations-bot Bot force-pushed the native-auth-as-json branch 2 times, most recently from 04dcf26 to 688a689 Compare October 6, 2026 18:09
@zaniebot zaniebot added the build:skip-release Disable building release binaries for a pull request label Oct 7, 2026
Comment thread crates/uv-auth/src/keyring/native.rs Outdated
Comment on lines +347 to +351
fn native_lock_directory() -> Result<PathBuf, Error> {
Ok(StateStore::from_settings(None)
.map_err(Error::NativeLockDirectory)?
.bucket(StateBucket::Credentials)
.join("native"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep native lock paths independent of XDG overrides

On Unix, processes with different XDG_DATA_HOME values can share the same native keyring, but StateStore::from_settings(None) gives them different lock directories. Both can then read the same realm JSON, add different accounts, and overwrite each other's updates. Ignoring UV_CREDENTIALS_DIR does not prevent this. Derive the lock location from the native store's user scope independently of configurable state directories, and cover this with a cross-process test.

Comment on lines +90 to +93
let json = Zeroizing::new(
serde_json::to_string(&credentials).map_err(Error::SerializeStoredCredentials)?,
);
entry.set_password(&json).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retain realm locks until native I/O completes

On macOS, Entry::set_password performs its write through spawn_blocking, which continues if the awaiting future is cancelled. Cancelling a legacy-migration request here drops its realm guard while the native write can still finish. Another process can acquire the lock, update the collection, and then have that update overwritten by the detached write. Keep lock ownership alive until the platform operation completes, including cancellation paths; the same requirement applies to collection removal.

Comment on lines +28 to +31
(Some(cli), None) => Some(cli),
(None, Some(url)) => Some(url.to_string()),
(None, None) if matches!(&backend, AuthBackend::System(_)) => None,
(None, None) => Some("__token__".to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve lookup of legacy token accounts

A token saved by an older uv version resides under the legacy service with username __token__. Passing None here makes native::fetch skip its entire legacy lookup loop, so the previously working uv auth token example.com now fails until the user supplies --username __token__ or logs in again. Preserve the legacy token fallback when username inference finds no credential, while retaining errors for ambiguous new-format accounts.

Comment thread crates/uv-auth/src/middleware.rs Outdated
Comment on lines +304 to +308
match self.cache().get_stored(url, username) {
Ok(credentials) => Ok(credentials),
Err(_) if self.preview.is_enabled(PreviewFeature::NativeAuth) => Err(
Self::native_store_error(crate::keyring::Error::AmbiguousUsername(url.clone())),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Defer cached ambiguity errors until authentication is needed

A successful authenticated request caches every credential in its realm. If another path has multiple saved accounts, this branch then rejects an authenticate = auto request before trying anonymous access—even when that endpoint is public. The same request succeeds when issued before the unrelated authenticated request. Treat ambiguity during eager cache lookup as a cache miss, and surface it when a challenge or authenticate = always requires credential selection.

Comment thread crates/uv/tests/it/auth.rs Outdated
Comment on lines +1984 to +1986
exit_code: 2 (failure)
----- stderr -----
error: Failed to fetch credentials for native-prefix-user@https://native-prefix.example.com/apiv1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Match the token diagnostic in the new snapshots

This expectation omits the backticks that commands/auth/token.rs includes around display_url; the default snapshot filters do not remove them. The same mismatch appears in native_auth_multiple_users and native_auth_logout_is_service_scoped, so these assertions cannot match the emitted diagnostic. Regenerate or correct these expectations while retaining the existing diagnostic formatting.

Comment on lines +1062 to +1065
cache_scope: if path_sensitive_store_enabled {
CredentialsCacheScope::FetchOnly
} else {
CredentialsCacheScope::Realm

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retain subprocess credentials as a realm fallback

When a plaintext store exists—even an empty file after logout—this disables realm caching for every subprocess result. For an authenticate = always index at /private/simple, a successful username-inferred lookup is cached under FetchUrl::Index. A protected wheel at /packages/... uses FetchUrl::Realm, finds no propagated credentials, and skips subprocess lookup without a username. This previously succeeded through the realm cache. Retain successfully authenticated subprocess credentials as a fallback consulted after path-scoped stores.

Comment thread crates/uv-auth/tests/native_cache.rs Outdated
@@ -0,0 +1,113 @@
#![cfg(any(target_os = "macos", target_os = "windows"))]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Feature-gate the native keyring integration tests

These platform gates make ordinary cargo test -p uv-auth access the real OS keyring on macOS and Windows. On machines without an unlocked keychain or suitable logon session, the tests can prompt or fail. Existing native tests in uv-keyring and uv/tests/it/auth.rs require the opt-in native-auth feature. Apply that convention to native_cache.rs, native_macos.rs, and native_windows.rs as well.

Comment on lines +928 to +931
let store_credentials = if let Some(text_store) = text_store {
debug!("Checking text store for credentials for `{url}`");
match text_store.get_credentials(
url,
credentials
.as_ref()
.and_then(|credentials| credentials.username()),
) {
Ok(credentials) => credentials.cloned(),
Err(err) => {
debug!("Failed to get credentials from text store: {err}");
let snapshot = StoredCredentials::from(text_store.realm_credentials(&realm));
match CredentialsCache::select_stored(&snapshot, url, &username) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Reuse plaintext realm snapshots across lookups

realm_credentials scans the entire plaintext store and clones every service and credential in the matching realm before selection. Username-bearing requests repeatedly reach this branch because complete_request_with_request_credentials never consults the stored snapshot cache, even after successful authentication populated it. The plaintext store is immutable after loading, so these requests unnecessarily rebuild the same snapshot. Reuse the shared realm snapshot and perform per-request matching against it.

Comment thread crates/uv-auth/tests/native_windows.rs Outdated
Comment on lines +94 to +98
for (url, credentials) in &entries {
let actual = provider.fetch(url, credentials.username()).await?;
if actual != Some(credentials.clone()) {
return Err(std::io::Error::other(format!(
"unexpected credentials returned for {url}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Separate independent Windows credential scenarios

The shared entries list combines bulk enumeration, account-name collisions, signed URL identities, and corrupt-entry tolerance, followed by a deletion check. Running these independent scenarios through one fetch loop couples their fixtures and prevents later checks from running when an earlier case fails. Keep the bulk population in the enumeration test, and move the identity and corruption regressions into separately named tests with explicit store/fetch assertions, following the repository's integration-test guidance.

@zaniebot
zaniebot deployed to automations October 9, 2026 18:50 — with GitHub Actions Active
zanieb and others added 28 commits October 9, 2026 23:02

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build:skip-release Disable building release binaries for a pull request enhancement New feature or request preview Experimental behavior priority:3 Normal: useful features, documentation, usability, or routine efficiency improvements. risk:3 Credible risk of meaningful incorrect behavior or regression; bounded or recoverable. size:5 Project-sized: too much semantic scope for a normal review; needs decomposition or design review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants