From 460ea6e9eb481f20459415590150d5530fbc2658 Mon Sep 17 00:00:00 2001
From: Zanie Blue
Date: Sun, 31 Aug 2025 15:14:51 -0500
Subject: [PATCH] Add `uv auth` commands (`login`, `logout`, and `token`)
(#15539)
Picks up the work from
- #14559
- https://github.com/astral-sh/uv/pull/14896
There are some high-level changes from those pull requests
1. We do not stash seen credentials in the keyring automatically
2. We use `auth login` and `auth logout` (for future consistency)
3. We add a `token` command for showing the credential that will be used
As well as many smaller changes to API, messaging, testing, etc.
---------
Co-authored-by: John Mumm
---
.config/nextest.toml | 7 +
Cargo.lock | 3 +
Cargo.toml | 1 +
crates/uv-auth/Cargo.toml | 2 +
crates/uv-auth/src/keyring.rs | 149 +++-
crates/uv-auth/src/middleware.rs | 1 +
crates/uv-cli/src/lib.rs | 95 ++-
crates/uv-client/src/base_client.rs | 3 +-
crates/uv-configuration/Cargo.toml | 1 +
crates/uv-configuration/src/authentication.rs | 57 ++
crates/uv-console/src/lib.rs | 13 +
crates/uv-preview/src/lib.rs | 3 +
crates/uv/Cargo.toml | 1 +
crates/uv/src/commands/auth/login.rs | 80 ++
crates/uv/src/commands/auth/logout.rs | 48 ++
crates/uv/src/commands/auth/mod.rs | 3 +
crates/uv/src/commands/auth/token.rs | 48 ++
crates/uv/src/commands/mod.rs | 4 +
crates/uv/src/lib.rs | 41 +-
crates/uv/src/settings.rs | 111 ++-
crates/uv/tests/it/auth.rs | 733 ++++++++++++++++++
crates/uv/tests/it/common/mod.rs | 36 +
crates/uv/tests/it/help.rs | 7 +
crates/uv/tests/it/main.rs | 3 +
crates/uv/tests/it/show_settings.rs | 4 +-
docs/reference/cli.md | 255 +++++-
uv.schema.json | 5 +
27 files changed, 1694 insertions(+), 20 deletions(-)
create mode 100644 crates/uv/src/commands/auth/login.rs
create mode 100644 crates/uv/src/commands/auth/logout.rs
create mode 100644 crates/uv/src/commands/auth/mod.rs
create mode 100644 crates/uv/src/commands/auth/token.rs
create mode 100644 crates/uv/tests/it/auth.rs
diff --git a/.config/nextest.toml b/.config/nextest.toml
index cc1a18dbe..a6c344d22 100644
--- a/.config/nextest.toml
+++ b/.config/nextest.toml
@@ -2,3 +2,10 @@
# Mark tests that take longer than 10s as slow.
# Terminate after 120s as a stop-gap measure to terminate on deadlock.
slow-timeout = { period = "10s", terminate-after = 12 }
+
+[test-groups]
+serial = { max-threads = 1 }
+
+[[profile.default.overrides]]
+filter = 'test(native_keyring)'
+test-group = 'serial'
diff --git a/Cargo.lock b/Cargo.lock
index 9daf3d3ec..0a572895c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5089,9 +5089,11 @@ dependencies = [
"serde",
"tempfile",
"test-log",
+ "thiserror 2.0.16",
"tokio",
"tracing",
"url",
+ "uv-keyring",
"uv-once-map",
"uv-redacted",
"uv-small-str",
@@ -5407,6 +5409,7 @@ dependencies = [
"uv-pep440",
"uv-pep508",
"uv-platform-tags",
+ "uv-redacted",
"uv-static",
]
diff --git a/Cargo.toml b/Cargo.toml
index c56753d4a..96c0b38d1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,6 +43,7 @@ uv-git-types = { path = "crates/uv-git-types" }
uv-globfilter = { path = "crates/uv-globfilter" }
uv-install-wheel = { path = "crates/uv-install-wheel", default-features = false }
uv-installer = { path = "crates/uv-installer" }
+uv-keyring = { path = "crates/uv-keyring" }
uv-logging = { path = "crates/uv-logging" }
uv-macros = { path = "crates/uv-macros" }
uv-metadata = { path = "crates/uv-metadata" }
diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml
index cbe4d4787..d3f288ab9 100644
--- a/crates/uv-auth/Cargo.toml
+++ b/crates/uv-auth/Cargo.toml
@@ -10,6 +10,7 @@ doctest = false
workspace = true
[dependencies]
+uv-keyring = { workspace = true, features = ["apple-native", "secret-service", "windows-native"] }
uv-once-map = { workspace = true }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true }
@@ -28,6 +29,7 @@ rust-netrc = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
+thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs
index 41b92114a..4ee9e0ba5 100644
--- a/crates/uv-auth/src/keyring.rs
+++ b/crates/uv-auth/src/keyring.rs
@@ -1,11 +1,14 @@
use std::{io::Write, process::Stdio};
use tokio::process::Command;
-use tracing::{instrument, trace, warn};
+use tracing::{debug, instrument, trace, warn};
use uv_redacted::DisplaySafeUrl;
use uv_warnings::warn_user_once;
use crate::credentials::Credentials;
+/// Service name prefix for storing credentials in a keyring.
+static UV_SERVICE_PREFIX: &str = "uv:";
+
/// A backend for retrieving credentials from a keyring.
///
/// See pip's implementation for reference
@@ -15,15 +18,47 @@ pub struct KeyringProvider {
backend: KeyringProviderBackend,
}
-#[derive(Debug)]
-pub(crate) enum KeyringProviderBackend {
- /// Use the `keyring` command to fetch credentials.
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error(transparent)]
+ Keyring(#[from] uv_keyring::Error),
+
+ #[error("The '{0}' keyring provider does not support storing credentials")]
+ StoreUnsupported(KeyringProviderBackend),
+
+ #[error("The '{0}' keyring provider does not support removing credentials")]
+ RemoveUnsupported(KeyringProviderBackend),
+}
+
+#[derive(Debug, Clone)]
+pub enum KeyringProviderBackend {
+ /// Use a native system keyring integration for credentials.
+ Native,
+ /// Use the external `keyring` command for credentials.
Subprocess,
#[cfg(test)]
Dummy(Vec<(String, &'static str, &'static str)>),
}
+impl std::fmt::Display for KeyringProviderBackend {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Native => write!(f, "native"),
+ Self::Subprocess => write!(f, "subprocess"),
+ #[cfg(test)]
+ Self::Dummy(_) => write!(f, "dummy"),
+ }
+ }
+}
+
impl KeyringProvider {
+ /// Create a new [`KeyringProvider::Native`].
+ pub fn native() -> Self {
+ Self {
+ backend: KeyringProviderBackend::Native,
+ }
+ }
+
/// Create a new [`KeyringProvider::Subprocess`].
pub fn subprocess() -> Self {
Self {
@@ -31,6 +66,84 @@ impl KeyringProvider {
}
}
+ /// Store credentials for the given [`DisplaySafeUrl`] to the keyring.
+ ///
+ /// Only [`KeyringProviderBackend::Native`] is supported at this time.
+ #[instrument(skip_all, fields(url = % url.to_string(), username))]
+ pub async fn store(
+ &self,
+ url: &DisplaySafeUrl,
+ credentials: &Credentials,
+ ) -> Result {
+ let Some(username) = credentials.username() else {
+ trace!("Unable to store credentials in keyring for {url} due to missing username");
+ return Ok(false);
+ };
+ let Some(password) = credentials.password() else {
+ trace!("Unable to store credentials in keyring for {url} due to missing password");
+ return Ok(false);
+ };
+
+ match &self.backend {
+ KeyringProviderBackend::Native => {
+ self.store_native(url.as_str(), username, password).await?;
+ Ok(true)
+ }
+ KeyringProviderBackend::Subprocess => {
+ Err(Error::StoreUnsupported(self.backend.clone()))
+ }
+ #[cfg(test)]
+ KeyringProviderBackend::Dummy(_) => Err(Error::StoreUnsupported(self.backend.clone())),
+ }
+ }
+
+ /// Store credentials to the system keyring.
+ #[instrument(skip(self))]
+ async fn store_native(
+ &self,
+ service: &str,
+ username: &str,
+ password: &str,
+ ) -> Result<(), Error> {
+ let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}");
+ let entry = uv_keyring::Entry::new(&prefixed_service, username)?;
+ entry.set_password(password).await?;
+ Ok(())
+ }
+
+ /// Remove credentials for the given [`DisplaySafeUrl`] and username from the keyring.
+ ///
+ /// Only [`KeyringProviderBackend::Native`] is supported at this time.
+ #[instrument(skip_all, fields(url = % url.to_string(), username))]
+ pub async fn remove(&self, url: &DisplaySafeUrl, username: &str) -> Result<(), Error> {
+ match &self.backend {
+ KeyringProviderBackend::Native => {
+ self.remove_native(url.as_str(), username).await?;
+ Ok(())
+ }
+ KeyringProviderBackend::Subprocess => {
+ Err(Error::RemoveUnsupported(self.backend.clone()))
+ }
+ #[cfg(test)]
+ KeyringProviderBackend::Dummy(_) => Err(Error::RemoveUnsupported(self.backend.clone())),
+ }
+ }
+
+ /// Remove credentials from the system keyring for the given `service_name`/`username`
+ /// pair.
+ #[instrument(skip(self))]
+ async fn remove_native(
+ &self,
+ service_name: &str,
+ username: &str,
+ ) -> Result<(), uv_keyring::Error> {
+ let prefixed_service = format!("{UV_SERVICE_PREFIX}{service_name}");
+ let entry = uv_keyring::Entry::new(&prefixed_service, username)?;
+ entry.delete_credential().await?;
+ trace!("Removed credentials for {username}@{service_name} from system keyring");
+ Ok(())
+ }
+
/// Fetch credentials for the given [`Url`] from the keyring.
///
/// Returns [`None`] if no password was found for the username or if any errors
@@ -55,6 +168,7 @@ impl KeyringProvider {
//
trace!("Checking keyring for URL {url}");
let mut credentials = match self.backend {
+ KeyringProviderBackend::Native => self.fetch_native(url.as_str(), username).await,
KeyringProviderBackend::Subprocess => {
self.fetch_subprocess(url.as_str(), username).await
}
@@ -72,6 +186,7 @@ impl KeyringProvider {
};
trace!("Checking keyring for host {host}");
credentials = match self.backend {
+ KeyringProviderBackend::Native => self.fetch_native(&host, username).await,
KeyringProviderBackend::Subprocess => self.fetch_subprocess(&host, username).await,
#[cfg(test)]
KeyringProviderBackend::Dummy(ref store) => {
@@ -175,6 +290,32 @@ impl KeyringProvider {
}
}
+ #[instrument(skip(self))]
+ async fn fetch_native(
+ &self,
+ service: &str,
+ username: Option<&str>,
+ ) -> Option<(String, String)> {
+ let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}");
+ let username = username?;
+ let Ok(entry) = uv_keyring::Entry::new(&prefixed_service, username) else {
+ return None;
+ };
+ match entry.get_password().await {
+ Ok(password) => return Some((username.to_string(), password)),
+ Err(uv_keyring::Error::NoEntry) => {
+ debug!("No entry found in system keyring for {service}");
+ }
+ Err(err) => {
+ warn_user_once!(
+ "Unable to fetch credentials for {service} from system keyring: {}",
+ err
+ );
+ }
+ }
+ None
+ }
+
#[cfg(test)]
fn fetch_dummy(
store: &Vec<(String, &'static str, &'static str)>,
diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs
index 28bbfb07a..40812f3ea 100644
--- a/crates/uv-auth/src/middleware.rs
+++ b/crates/uv-auth/src/middleware.rs
@@ -375,6 +375,7 @@ impl AuthMiddleware {
.as_ref()
.is_ok_and(|response| response.error_for_status_ref().is_ok())
{
+ // TODO(zanieb): Consider also updating the system keyring after successful use
trace!("Updating cached credentials for {url} to {credentials:?}");
self.cache().insert(&url, credentials);
}
diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs
index b14b2a62a..7a5e9ac5b 100644
--- a/crates/uv-cli/src/lib.rs
+++ b/crates/uv-cli/src/lib.rs
@@ -11,7 +11,7 @@ use clap::{Args, Parser, Subcommand};
use uv_cache::CacheArgs;
use uv_configuration::{
ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend,
- TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
+ Service, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
};
use uv_distribution_types::{
ConfigSettingEntry, ConfigSettingPackageEntry, Index, IndexUrl, Origin, PipExtraIndex,
@@ -399,6 +399,13 @@ impl From for anstream::ColorChoice {
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum Commands {
+ /// Manage authentication.
+ #[command(
+ after_help = "Use `uv help auth` for more details.",
+ after_long_help = ""
+ )]
+ Auth(AuthNamespace),
+
/// Manage Python projects.
#[command(flatten)]
Project(Box),
@@ -4386,6 +4393,22 @@ pub struct FormatArgs {
pub extra_args: Vec,
}
+#[derive(Args)]
+pub struct AuthNamespace {
+ #[command(subcommand)]
+ pub command: AuthCommand,
+}
+
+#[derive(Subcommand)]
+pub enum AuthCommand {
+ /// Login to a service
+ Login(AuthLoginArgs),
+ /// Logout of a service
+ Logout(AuthLogoutArgs),
+ /// Show the authentication token for a service
+ Token(AuthTokenArgs),
+}
+
#[derive(Args)]
pub struct ToolNamespace {
#[command(subcommand)]
@@ -5501,6 +5524,76 @@ pub struct PythonPinArgs {
pub rm: bool,
}
+#[derive(Args)]
+pub struct AuthLogoutArgs {
+ /// The service to logout of.
+ pub service: Service,
+
+ /// The username to logout.
+ #[arg(long, short)]
+ pub username: Option,
+
+ /// The keyring provider to use for storage of credentials.
+ ///
+ /// Only `--keyring-provider native` is supported for `logout`, which uses the system keyring
+ /// via an integration built into uv.
+ #[arg(
+ long,
+ value_enum,
+ env = EnvVars::UV_KEYRING_PROVIDER,
+ )]
+ pub keyring_provider: Option,
+}
+
+#[derive(Args)]
+pub struct AuthLoginArgs {
+ /// The service to login to.
+ pub service: Service,
+
+ /// The username to use for the service.
+ #[arg(long, short, conflicts_with = "token")]
+ pub username: Option,
+
+ /// The password to use for the service.
+ #[arg(long, conflicts_with = "token")]
+ pub password: Option,
+
+ /// The token to use for the service.
+ ///
+ /// The username will be set to `__token__`.
+ #[arg(long, short, conflicts_with = "username", conflicts_with = "password")]
+ pub token: Option,
+
+ /// The keyring provider to use for storage of credentials.
+ ///
+ /// Only `--keyring-provider native` is supported for `login`, which uses the system keyring via
+ /// an integration built into uv.
+ #[arg(
+ long,
+ value_enum,
+ env = EnvVars::UV_KEYRING_PROVIDER,
+ )]
+ pub keyring_provider: Option,
+}
+
+#[derive(Args)]
+pub struct AuthTokenArgs {
+ /// The service to lookup.
+ pub service: Service,
+
+ /// The username to lookup.
+ #[arg(long, short)]
+ pub username: Option,
+
+ /// The keyring provider to use for reading credentials.
+ #[arg(
+ long,
+ value_enum,
+ env = EnvVars::UV_KEYRING_PROVIDER,
+ )]
+ pub keyring_provider: Option,
+}
+
#[derive(Args)]
pub struct GenerateShellCompletionArgs {
/// The shell to generate the completion script for
diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs
index 55712470f..641d88498 100644
--- a/crates/uv-client/src/base_client.rs
+++ b/crates/uv-client/src/base_client.rs
@@ -28,8 +28,7 @@ use tracing::{debug, trace};
use url::ParseError;
use url::Url;
-use uv_auth::Credentials;
-use uv_auth::{AuthMiddleware, Indexes};
+use uv_auth::{AuthMiddleware, Credentials, Indexes};
use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified;
use uv_pep508::MarkerEnvironment;
diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml
index 655a115a9..3776ab8c1 100644
--- a/crates/uv-configuration/Cargo.toml
+++ b/crates/uv-configuration/Cargo.toml
@@ -25,6 +25,7 @@ uv-normalize = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true, features = ["schemars"] }
uv-platform-tags = { workspace = true }
+uv-redacted = { workspace = true }
uv-static = { workspace = true }
clap = { workspace = true, features = ["derive"], optional = true }
either = { workspace = true }
diff --git a/crates/uv-configuration/src/authentication.rs b/crates/uv-configuration/src/authentication.rs
index a6773c81f..dee874dae 100644
--- a/crates/uv-configuration/src/authentication.rs
+++ b/crates/uv-configuration/src/authentication.rs
@@ -1,4 +1,6 @@
+use std::str::FromStr;
use uv_auth::{self, KeyringProvider};
+use uv_redacted::DisplaySafeUrl;
/// Keyring provider type to use for credential lookup.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -9,6 +11,8 @@ pub enum KeyringProviderType {
/// Do not use keyring for credential lookup.
#[default]
Disabled,
+ /// Use a native integration with the system keychain for credential lookup.
+ Native,
/// Use the `keyring` command for credential lookup.
Subprocess,
// /// Not yet implemented
@@ -22,7 +26,60 @@ impl KeyringProviderType {
pub fn to_provider(&self) -> Option {
match self {
Self::Disabled => None,
+ Self::Native => Some(KeyringProvider::native()),
Self::Subprocess => Some(KeyringProvider::subprocess()),
}
}
}
+
+impl std::fmt::Display for KeyringProviderType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Disabled => write!(f, "disabled"),
+ Self::Native => write!(f, "native"),
+ Self::Subprocess => write!(f, "subprocess"),
+ }
+ }
+}
+
+/// A service URL that wraps [`DisplaySafeUrl`] for CLI usage.
+///
+/// This type provides automatic URL parsing and validation when used as a CLI argument,
+/// eliminating the need for manual parsing in command functions.
+#[derive(Debug, Clone)]
+pub struct Service(DisplaySafeUrl);
+
+impl Service {
+ /// Get the underlying [`DisplaySafeUrl`].
+ pub fn url(&self) -> &DisplaySafeUrl {
+ &self.0
+ }
+
+ /// Convert into the underlying [`DisplaySafeUrl`].
+ pub fn into_url(self) -> DisplaySafeUrl {
+ self.0
+ }
+}
+
+impl FromStr for Service {
+ type Err = url::ParseError;
+
+ fn from_str(s: &str) -> Result {
+ // First try parsing as-is
+ match DisplaySafeUrl::parse(s) {
+ Ok(url) => Ok(Self(url)),
+ Err(url::ParseError::RelativeUrlWithoutBase) => {
+ // If it's a relative URL, try prepending https://
+ let with_https = format!("https://{s}");
+ DisplaySafeUrl::parse(&with_https).map(Service)
+ }
+ Err(e) => Err(e),
+ }
+ }
+}
+
+impl std::fmt::Display for Service {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
diff --git a/crates/uv-console/src/lib.rs b/crates/uv-console/src/lib.rs
index f90d45c1f..d1476f5c7 100644
--- a/crates/uv-console/src/lib.rs
+++ b/crates/uv-console/src/lib.rs
@@ -112,6 +112,19 @@ pub fn password(prompt: &str, term: &Term) -> std::io::Result {
Ok(input)
}
+/// Prompt the user for username in the given [`Term`].
+pub fn username(prompt: &str, term: &Term) -> std::io::Result {
+ term.write_str(prompt)?;
+ term.show_cursor()?;
+ term.flush()?;
+
+ let input = term.read_line()?;
+
+ term.clear_line()?;
+
+ Ok(input)
+}
+
/// Prompt the user for input text in the given [`Term`].
///
/// This is a slimmed-down version of `dialoguer::Input`.
diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs
index 59c680264..ad30a4c71 100644
--- a/crates/uv-preview/src/lib.rs
+++ b/crates/uv-preview/src/lib.rs
@@ -18,6 +18,7 @@ bitflags::bitflags! {
const EXTRA_BUILD_DEPENDENCIES = 1 << 6;
const DETECT_MODULE_CONFLICTS = 1 << 7;
const FORMAT = 1 << 8;
+ const NATIVE_KEYRING = 1 << 9;
}
}
@@ -36,6 +37,7 @@ impl PreviewFeatures {
Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts",
Self::FORMAT => "format",
+ Self::NATIVE_KEYRING => "native-keyring",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
@@ -82,6 +84,7 @@ impl FromStr for PreviewFeatures {
"extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
"detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS,
"format" => Self::FORMAT,
+ "native-keyring" => Self::NATIVE_KEYRING,
_ => {
warn_user_once!("Unknown preview feature: `{part}`");
continue;
diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml
index 48cd8a93a..e38a07ef7 100644
--- a/crates/uv/Cargo.toml
+++ b/crates/uv/Cargo.toml
@@ -152,6 +152,7 @@ ignored = [
[features]
default = ["performance", "uv-distribution/static", "default-tests"]
+keyring-tests = []
# Use better memory allocators, etc.
performance = ["performance-memory-allocator"]
performance-memory-allocator = ["dep:uv-performance-memory-allocator"]
diff --git a/crates/uv/src/commands/auth/login.rs b/crates/uv/src/commands/auth/login.rs
new file mode 100644
index 000000000..fb67e3fbc
--- /dev/null
+++ b/crates/uv/src/commands/auth/login.rs
@@ -0,0 +1,80 @@
+use anyhow::{Result, bail};
+use std::fmt::Write;
+
+use console::Term;
+use uv_auth::Credentials;
+use uv_configuration::{KeyringProviderType, Service};
+
+use crate::{commands::ExitStatus, printer::Printer};
+
+/// Login to a service.
+pub(crate) async fn login(
+ service: Service,
+ username: Option,
+ password: Option,
+ token: Option,
+ keyring_provider: Option,
+ printer: Printer,
+) -> Result {
+ let url = service.url();
+ let display_url = username
+ .as_ref()
+ .map(|username| format!("{username}@{url}"))
+ .unwrap_or_else(|| url.to_string());
+
+ let username = if let Some(username) = username {
+ username
+ } else if token.is_some() {
+ String::from("__token__")
+ } else {
+ let term = Term::stderr();
+ if term.is_term() {
+ let prompt = "username: ";
+ uv_console::username(prompt, &term)?
+ } else {
+ bail!("No username provided; did you mean to provide `--username` or `--token`?");
+ }
+ };
+
+ // Be helpful about incompatible `keyring-provider` settings
+ let Some(keyring_provider) = &keyring_provider else {
+ bail!(
+ "Logging in requires setting `keyring-provider = {}` for credentials to be retrieved in subsequent commands",
+ KeyringProviderType::Native
+ );
+ };
+ let provider = match keyring_provider {
+ KeyringProviderType::Native => keyring_provider.to_provider().unwrap(),
+ KeyringProviderType::Disabled | KeyringProviderType::Subprocess => {
+ bail!(
+ "Cannot login with `keyring-provider = {keyring_provider}`, use `keyring-provider = {}` instead",
+ KeyringProviderType::Native
+ );
+ }
+ };
+
+ // FIXME: It would be preferable to accept the value of --password or --token
+ // from stdin, perhaps checking here for `-` as an indicator to read stdin. We
+ // could then warn if the password is provided as a plaintext argument.
+ let password = if let Some(password) = password {
+ password
+ } else if let Some(token) = token {
+ token
+ } else {
+ let term = Term::stderr();
+ if term.is_term() {
+ let prompt = "password: ";
+ uv_console::password(prompt, &term)?
+ } else {
+ bail!("No password provided; did you mean to provide `--password` or `--token`?");
+ }
+ };
+
+ // TODO(zanieb): Add support for other authentication schemes here, e.g., `Credentials::Bearer`
+ let credentials = Credentials::basic(Some(username), Some(password));
+ provider.store(url, &credentials).await?;
+
+ writeln!(printer.stderr(), "Logged in to {display_url}")?;
+
+ Ok(ExitStatus::Success)
+}
diff --git a/crates/uv/src/commands/auth/logout.rs b/crates/uv/src/commands/auth/logout.rs
new file mode 100644
index 000000000..270dfdade
--- /dev/null
+++ b/crates/uv/src/commands/auth/logout.rs
@@ -0,0 +1,48 @@
+use anyhow::{Context, Result, bail};
+use std::{borrow::Cow, fmt::Write};
+use uv_configuration::{KeyringProviderType, Service};
+
+use crate::{commands::ExitStatus, printer::Printer};
+
+/// Logout from a service.
+///
+/// If no username is provided, defaults to `__token__`.
+pub(crate) async fn logout(
+ service: Service,
+ username: Option,
+ keyring_provider: Option,
+ printer: Printer,
+) -> Result {
+ let url = service.url();
+ let display_url = username
+ .as_ref()
+ .map(|username| format!("{username}@{url}"))
+ .unwrap_or_else(|| url.to_string());
+ let username = username
+ .map(Cow::Owned)
+ .unwrap_or(Cow::Borrowed("__token__"));
+
+ // Unlike login, we'll default to the native provider if none is requested since it's the only
+ // valid option and it doesn't matter if the credentials are available in subsequent commands.
+ let keyring_provider = keyring_provider.unwrap_or(KeyringProviderType::Native);
+
+ // Be helpful about incompatible `keyring-provider` settings
+ let provider = match keyring_provider {
+ KeyringProviderType::Native => keyring_provider.to_provider().unwrap(),
+ KeyringProviderType::Disabled | KeyringProviderType::Subprocess => {
+ bail!(
+ "Cannot logout with `keyring-provider = {keyring_provider}`, use `keyring-provider = {}` instead",
+ KeyringProviderType::Native
+ );
+ }
+ };
+
+ provider
+ .remove(url, &username)
+ .await
+ .with_context(|| format!("Unable to remove credentials for {display_url}"))?;
+
+ writeln!(printer.stderr(), "Logged out of {display_url}")?;
+
+ Ok(ExitStatus::Success)
+}
diff --git a/crates/uv/src/commands/auth/mod.rs b/crates/uv/src/commands/auth/mod.rs
new file mode 100644
index 000000000..8904696da
--- /dev/null
+++ b/crates/uv/src/commands/auth/mod.rs
@@ -0,0 +1,3 @@
+pub(crate) mod login;
+pub(crate) mod logout;
+pub(crate) mod token;
diff --git a/crates/uv/src/commands/auth/token.rs b/crates/uv/src/commands/auth/token.rs
new file mode 100644
index 000000000..9cd2db86e
--- /dev/null
+++ b/crates/uv/src/commands/auth/token.rs
@@ -0,0 +1,48 @@
+use std::fmt::Write;
+
+use anyhow::{Context, Result, bail};
+
+use uv_configuration::{KeyringProviderType, Service};
+
+use crate::{Printer, commands::ExitStatus};
+
+/// Show the token that will be used for a service.
+pub(crate) async fn token(
+ service: Service,
+ username: Option,
+ keyring_provider: Option,
+ printer: Printer,
+) -> Result {
+ // Determine the keyring provider to use
+ let Some(keyring_provider) = &keyring_provider else {
+ bail!("Retrieving credentials requires setting a `keyring-provider`");
+ };
+ let Some(provider) = keyring_provider.to_provider() else {
+ bail!("Cannot retrieve credentials with `keyring-provider = {keyring_provider}`");
+ };
+
+ let url = service.url();
+ let display_url = username
+ .as_ref()
+ .map(|username| format!("{username}@{url}"))
+ .unwrap_or_else(|| url.to_string());
+
+ let credentials = provider
+ .fetch(url, Some(username.as_deref().unwrap_or("__token__")))
+ .await
+ .with_context(|| format!("Failed to fetch credentials for {display_url}"))?;
+
+ let Some(password) = credentials.password() else {
+ bail!(
+ "No {} found for {display_url}",
+ if username.is_some() {
+ "password"
+ } else {
+ "token"
+ }
+ );
+ };
+
+ writeln!(printer.stdout(), "{password}")?;
+ Ok(ExitStatus::Success)
+}
diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs
index 42038976d..c0a9a8ce7 100644
--- a/crates/uv/src/commands/mod.rs
+++ b/crates/uv/src/commands/mod.rs
@@ -9,6 +9,9 @@ use anyhow::Context;
use owo_colors::OwoColorize;
use tracing::debug;
+pub(crate) use auth::login::login as auth_login;
+pub(crate) use auth::logout::logout as auth_logout;
+pub(crate) use auth::token::token as auth_token;
pub(crate) use build_frontend::build_frontend;
pub(crate) use cache_clean::cache_clean;
pub(crate) use cache_dir::cache_dir;
@@ -65,6 +68,7 @@ pub(crate) use venv::venv;
use crate::printer::Printer;
+mod auth;
pub(crate) mod build_backend;
mod build_frontend;
mod cache_clean;
diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs
index 812ad9ada..ab866d57b 100644
--- a/crates/uv/src/lib.rs
+++ b/crates/uv/src/lib.rs
@@ -24,9 +24,9 @@ use uv_cache_info::Timestamp;
#[cfg(feature = "self-update")]
use uv_cli::SelfUpdateArgs;
use uv_cli::{
- BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace,
- ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand,
- ToolNamespace, TopLevelArgs, compat::CompatArgs,
+ AuthCommand, AuthNamespace, BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands,
+ PipCommand, PipNamespace, ProjectCommand, PythonCommand, PythonNamespace, SelfCommand,
+ SelfNamespace, ToolCommand, ToolNamespace, TopLevelArgs, compat::CompatArgs,
};
use uv_client::BaseClientBuilder;
use uv_configuration::min_stack_size;
@@ -439,6 +439,41 @@ async fn run(mut cli: Cli) -> Result {
.retries_from_env()?;
match *cli.command {
+ Commands::Auth(AuthNamespace {
+ command: AuthCommand::Login(args),
+ }) => {
+ // Resolve the settings from the command-line arguments and workspace configuration.
+ let args = settings::AuthLoginSettings::resolve(args, filesystem);
+ show_settings!(args);
+
+ commands::auth_login(
+ args.service,
+ args.username,
+ args.password,
+ args.token,
+ args.keyring_provider,
+ printer,
+ )
+ .await
+ }
+ Commands::Auth(AuthNamespace {
+ command: AuthCommand::Logout(args),
+ }) => {
+ // Resolve the settings from the command-line arguments and workspace configuration.
+ let args = settings::AuthLogoutSettings::resolve(args, filesystem);
+ show_settings!(args);
+
+ commands::auth_logout(args.service, args.username, args.keyring_provider, printer).await
+ }
+ Commands::Auth(AuthNamespace {
+ command: AuthCommand::Token(args),
+ }) => {
+ // Resolve the settings from the command-line arguments and workspace configuration.
+ let args = settings::AuthTokenSettings::resolve(args, filesystem);
+ show_settings!(args);
+
+ commands::auth_token(args.service, args.username, args.keyring_provider, printer).await
+ }
Commands::Help(args) => commands::help(
args.command.unwrap_or_default().as_slice(),
printer,
diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs
index 7a44aa6e0..cf939f4e3 100644
--- a/crates/uv/src/settings.rs
+++ b/crates/uv/src/settings.rs
@@ -7,12 +7,13 @@ use std::str::FromStr;
use uv_cache::{CacheArgs, Refresh};
use uv_cli::comma::CommaSeparatedRequirements;
use uv_cli::{
- AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe,
- PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
- PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
- PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
- SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
- ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat,
+ AddArgs, AuthLoginArgs, AuthLogoutArgs, AuthTokenArgs, ColorChoice, ExternalCommand,
+ GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
+ PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs,
+ PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonListFormat, PythonPinArgs,
+ PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, SyncArgs, SyncFormat, ToolDirArgs,
+ ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs,
+ VersionBump, VersionFormat,
};
use uv_cli::{
AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs,
@@ -24,7 +25,8 @@ use uv_configuration::{
BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode,
ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions,
KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion,
- SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem,
+ Service, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade,
+ VersionControlSystem,
};
use uv_distribution_types::{
ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations, IndexUrl,
@@ -3483,6 +3485,101 @@ impl PublishSettings {
}
}
+/// The resolved settings to use for an invocation of the `uv auth logout` CLI.
+#[derive(Debug, Clone)]
+pub(crate) struct AuthLogoutSettings {
+ pub(crate) service: Service,
+ pub(crate) username: Option,
+
+ // Both CLI and configuration.
+ pub(crate) keyring_provider: Option,
+}
+
+impl AuthLogoutSettings {
+ /// Resolve the [`AuthLogoutSettings`] from the CLI and filesystem configuration.
+ pub(crate) fn resolve(args: AuthLogoutArgs, filesystem: Option) -> Self {
+ let Options { top_level, .. } = filesystem
+ .map(FilesystemOptions::into_options)
+ .unwrap_or_default();
+
+ let ResolverInstallerSchema {
+ keyring_provider, ..
+ } = top_level;
+
+ Self {
+ service: args.service,
+ username: args.username,
+ keyring_provider,
+ }
+ }
+}
+
+/// The resolved settings to use for an invocation of the `uv auth token` CLI.
+#[derive(Debug, Clone)]
+pub(crate) struct AuthTokenSettings {
+ pub(crate) service: Service,
+ pub(crate) username: Option,
+
+ // Both CLI and configuration.
+ pub(crate) keyring_provider: Option,
+}
+
+impl AuthTokenSettings {
+ /// Resolve the [`AuthTokenSettings`] from the CLI and filesystem configuration.
+ pub(crate) fn resolve(args: AuthTokenArgs, filesystem: Option) -> Self {
+ let Options { top_level, .. } = filesystem
+ .map(FilesystemOptions::into_options)
+ .unwrap_or_default();
+
+ let ResolverInstallerSchema {
+ keyring_provider, ..
+ } = top_level;
+
+ let keyring_provider = args.keyring_provider.combine(keyring_provider);
+
+ Self {
+ service: args.service,
+ username: args.username,
+ keyring_provider,
+ }
+ }
+}
+
+/// The resolved settings to use for an invocation of the `uv auth set` CLI.
+#[derive(Debug, Clone)]
+pub(crate) struct AuthLoginSettings {
+ pub(crate) service: Service,
+ pub(crate) username: Option,
+ pub(crate) password: Option,
+ pub(crate) token: Option,
+
+ // Both CLI and configuration.
+ pub(crate) keyring_provider: Option,
+}
+
+impl AuthLoginSettings {
+ /// Resolve the [`AuthLoginSettings`] from the CLI and filesystem configuration.
+ pub(crate) fn resolve(args: AuthLoginArgs, filesystem: Option) -> Self {
+ let Options { top_level, .. } = filesystem
+ .map(FilesystemOptions::into_options)
+ .unwrap_or_default();
+
+ let ResolverInstallerSchema {
+ keyring_provider, ..
+ } = top_level;
+
+ let keyring_provider = args.keyring_provider.combine(keyring_provider);
+
+ Self {
+ service: args.service,
+ username: args.username,
+ password: args.password,
+ token: args.token,
+ keyring_provider,
+ }
+ }
+}
+
// Environment variables that are not exposed as CLI arguments.
mod env {
use uv_static::EnvVars;
diff --git a/crates/uv/tests/it/auth.rs b/crates/uv/tests/it/auth.rs
new file mode 100644
index 000000000..0532c2dad
--- /dev/null
+++ b/crates/uv/tests/it/auth.rs
@@ -0,0 +1,733 @@
+use anyhow::Result;
+use assert_cmd::assert::OutputAssertExt;
+use assert_fs::{fixture::PathChild, prelude::FileWriteStr};
+use uv_static::EnvVars;
+
+use crate::common::{TestContext, uv_snapshot, venv_bin_path};
+
+#[test]
+fn add_package_native_keyring() -> Result<()> {
+ let context = TestContext::new("3.12").with_real_home();
+
+ // Clear state before the test
+ context
+ .auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .status()?;
+
+ // Configure `pyproject.toml` with native keyring provider.
+ let pyproject_toml = context.temp_dir.child("pyproject.toml");
+ pyproject_toml.write_str(indoc::indoc! { r#"
+ [project]
+ name = "foo"
+ version = "1.0.0"
+ requires-python = ">=3.11, <4"
+ dependencies = []
+
+ [tool.uv]
+ keyring-provider = "native"
+ "#
+ })?;
+
+ // Try to add a package without credentials.
+ uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ × No solution found when resolving dependencies:
+ ╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable.
+
+ hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
+ help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
+ "
+ );
+
+ // Login to the index
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--password")
+ .arg("heron"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Try to add the original package without credentials again. This should use
+ // credentials storied in the system keyring.
+ uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 4 packages in [TIME]
+ Prepared 3 packages in [TIME]
+ Installed 3 packages in [TIME]
+ + anyio==4.3.0
+ + idna==3.6
+ + sniffio==1.3.1
+ "
+ );
+
+ // Logout of the index
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged out of public@https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Authentication should fail again
+ uv_snapshot!(context.add().arg("iniconfig").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r"
+ success: false
+ exit_code: 1
+ ----- stdout -----
+
+ ----- stderr -----
+ × No solution found when resolving dependencies:
+ ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable.
+
+ hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
+ help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
+ "
+ );
+
+ Ok(())
+}
+
+#[test]
+fn token_native_keyring() -> Result<()> {
+ let context = TestContext::new_with_versions(&[]).with_real_home();
+
+ // Clear state before the test
+ context
+ .auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .status()?;
+
+ // Without a service name
+ uv_snapshot!(context.auth_token(), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: the following required arguments were not provided:
+
+
+ Usage: uv auth token --cache-dir [CACHE_DIR]
+
+ For more information, try '--help'.
+ ");
+
+ // Without a keyring provider...
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Retrieving credentials requires setting a `keyring-provider`
+ ");
+
+ // Without persisted credentials
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ // Without persisted credentials (with a username in the request)
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to fetch credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ // Login to the index
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--password")
+ .arg("heron")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Show the credentials
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ heron
+
+ ----- stderr -----
+ ");
+
+ // Without the username
+ // TODO(zanieb): Add a hint here if we can?
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ // With a mismatched username
+ // TODO(zanieb): Add a hint here if we can?
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("private")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to fetch credentials for private@https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ // Login to the index with a token
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--token")
+ .arg("heron")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Retrieve the token without a username
+ uv_snapshot!(context.auth_token()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ heron
+
+ ----- stderr -----
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn token_subprocess_keyring() {
+ let context = TestContext::new("3.12");
+
+ // Without a keyring on the PATH
+ uv_snapshot!(context.auth_token()
+ .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("subprocess"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Failed to fetch credentials for https://****@pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Install our keyring plugin
+ context
+ .pip_install()
+ .arg(
+ context
+ .workspace_root
+ .join("scripts")
+ .join("packages")
+ .join("keyring_test_plugin"),
+ )
+ .assert()
+ .success();
+
+ // Without credentials available
+ uv_snapshot!(context.auth_token()
+ .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("subprocess")
+ .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ Keyring request for __token__@https://public@pypi-proxy.fly.dev/basic-auth/simple
+ Keyring request for __token__@pypi-proxy.fly.dev
+ error: Failed to fetch credentials for https://****@pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Without a username
+ // TODO(zanieb): Add a hint here if we can?
+ uv_snapshot!(context.auth_token()
+ .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("subprocess")
+ .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#)
+ .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ Keyring request for __token__@https://public@pypi-proxy.fly.dev/basic-auth/simple
+ Keyring request for __token__@pypi-proxy.fly.dev
+ error: Failed to fetch credentials for https://****@pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // With the correct username
+ uv_snapshot!(context.auth_token()
+ .arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("subprocess")
+ .arg("--username")
+ .arg("public")
+ .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#)
+ .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ heron
+
+ ----- stderr -----
+ Keyring request for public@https://public@pypi-proxy.fly.dev/basic-auth/simple
+ Keyring request for public@pypi-proxy.fly.dev
+ "
+ );
+}
+
+#[test]
+fn login_native_keyring() -> Result<()> {
+ let context = TestContext::new_with_versions(&[]).with_real_home();
+
+ // Clear state before the test
+ context
+ .auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .status()?;
+
+ // Without a service name
+ uv_snapshot!(context.auth_login(), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: the following required arguments were not provided:
+
+
+ Usage: uv auth login --cache-dir [CACHE_DIR]
+
+ For more information, try '--help'.
+ ");
+
+ // Without a username (or token)
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: No username provided; did you mean to provide `--username` or `--token`?
+ ");
+
+ // Without a password
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: No password provided; did you mean to provide `--password` or `--token`?
+ ");
+
+ // Without a keyring provider
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--token")
+ .arg("foo"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Logging in requires setting `keyring-provider = native` for credentials to be retrieved in subsequent commands
+ ");
+
+ // Successful
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--password")
+ .arg("heron")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ Ok(())
+}
+
+#[test]
+fn login_token_native_keyring() -> Result<()> {
+ let context = TestContext::new_with_versions(&[]).with_real_home();
+
+ // Clear state before the test
+ context
+ .auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("__token__")
+ .status()?;
+
+ // Successful with token
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--token")
+ .arg("test-token")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ Ok(())
+}
+
+#[test]
+fn logout_native_keyring() -> Result<()> {
+ let context = TestContext::new_with_versions(&[]).with_real_home();
+
+ // Clear state before the test
+ context
+ .auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .status()?;
+
+ // Without a service name
+ uv_snapshot!(context.auth_logout(), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: the following required arguments were not provided:
+
+
+ Usage: uv auth logout --cache-dir [CACHE_DIR]
+
+ For more information, try '--help'.
+ ");
+
+ // Logout without a keyring provider
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged out of https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ // Logout before logging in (without a username)
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth/simple
+ Caused by: No matching entry found in secure storage
+ ");
+
+ // Logout before logging in (with a username)
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Unable to remove credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
+ Caused by: No matching entry found in secure storage
+ ");
+
+ // Login with a username
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--password")
+ .arg("heron")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to public@https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Logout without a username
+ // TODO(zanieb): Add a hint here if we can?
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth/simple
+ Caused by: No matching entry found in secure storage
+ ");
+
+ // Logout with a username
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--username")
+ .arg("public")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged out of public@https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn logout_token_native_keyring() -> Result<()> {
+ let context = TestContext::new_with_versions(&[]).with_real_home();
+
+ // Clear state before the test
+ context
+ .auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .status()?;
+
+ // Login with a token
+ uv_snapshot!(context.auth_login()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--token")
+ .arg("test-token")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to https://pypi-proxy.fly.dev/basic-auth/simple
+ "
+ );
+
+ // Logout without a username
+ uv_snapshot!(context.auth_logout()
+ .arg("https://pypi-proxy.fly.dev/basic-auth/simple")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged out of https://pypi-proxy.fly.dev/basic-auth/simple
+ ");
+
+ Ok(())
+}
+
+#[test]
+fn login_url_parsing() {
+ let context = TestContext::new_with_versions(&[]).with_real_home();
+
+ // A domain-only service name gets https:// prepended
+ uv_snapshot!(context.auth_login()
+ .arg("example.com")
+ .arg("--username")
+ .arg("test")
+ .arg("--password")
+ .arg("test")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to test@https://example.com/
+ ");
+
+ // When including a protocol explicitly, it is retained
+ uv_snapshot!(context.auth_login()
+ .arg("http://example.com")
+ .arg("--username")
+ .arg("test")
+ .arg("--password")
+ .arg("test")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to test@http://example.com/
+ ");
+
+ uv_snapshot!(context.auth_login()
+ .arg("https://example.com")
+ .arg("--username")
+ .arg("test")
+ .arg("--password")
+ .arg("test")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to test@https://example.com/
+ ");
+
+ // A domain-only service with a path also gets https:// prepended
+ uv_snapshot!(context.auth_login()
+ .arg("example.com/simple")
+ .arg("--username")
+ .arg("test")
+ .arg("--password")
+ .arg("test")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Logged in to test@https://example.com/simple
+ ");
+
+ // An invalid URL is rejected
+ uv_snapshot!(context.auth_login()
+ .arg("not a valid url")
+ .arg("--username")
+ .arg("test")
+ .arg("--password")
+ .arg("test")
+ .arg("--keyring-provider")
+ .arg("native"), @r"
+ success: false
+ exit_code: 2
+ ----- stdout -----
+
+ ----- stderr -----
+ error: invalid value 'not a valid url' for '': invalid international domain name
+
+ For more information, try '--help'.
+ ");
+}
diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs
index 0739d8591..5b518cd8f 100644
--- a/crates/uv/tests/it/common/mod.rs
+++ b/crates/uv/tests/it/common/mod.rs
@@ -1274,6 +1274,42 @@ impl TestContext {
command
}
+ /// Create a `uv auth login` command.
+ pub fn auth_login(&self) -> Command {
+ let mut command = Self::new_command();
+ command.arg("auth").arg("login");
+ self.add_shared_options(&mut command, false);
+ command
+ }
+
+ /// Create a `uv auth logout` command.
+ pub fn auth_logout(&self) -> Command {
+ let mut command = Self::new_command();
+ command.arg("auth").arg("logout");
+ self.add_shared_options(&mut command, false);
+ command
+ }
+
+ /// Create a `uv auth token` command.
+ pub fn auth_token(&self) -> Command {
+ let mut command = Self::new_command();
+ command.arg("auth").arg("token");
+ self.add_shared_options(&mut command, false);
+ command
+ }
+
+ /// Set `HOME` to the real home directory.
+ ///
+ /// We need this for testing commands which use the macOS keychain.
+ #[must_use]
+ pub fn with_real_home(mut self) -> Self {
+ if let Some(home) = env::var_os(EnvVars::HOME) {
+ self.extra_env
+ .push((EnvVars::HOME.to_string().into(), home));
+ }
+ self
+ }
+
/// Run the given python code and check whether it succeeds.
pub fn assert_command(&self, command: &str) -> Assert {
self.python_command()
diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs
index 5231f70d5..6a2e9bc48 100644
--- a/crates/uv/tests/it/help.rs
+++ b/crates/uv/tests/it/help.rs
@@ -16,6 +16,7 @@ fn help() {
Usage: uv [OPTIONS]
Commands:
+ auth Manage authentication
run Run a command or script
init Create a new project
add Add dependencies to the project
@@ -97,6 +98,7 @@ fn help_flag() {
Usage: uv [OPTIONS]
Commands:
+ auth Manage authentication
run Run a command or script
init Create a new project
add Add dependencies to the project
@@ -176,6 +178,7 @@ fn help_short_flag() {
Usage: uv [OPTIONS]
Commands:
+ auth Manage authentication
run Run a command or script
init Create a new project
add Add dependencies to the project
@@ -874,6 +877,7 @@ fn help_unknown_subcommand() {
----- stderr -----
error: There is no command `foobar` for `uv`. Did you mean one of:
+ auth
run
init
add
@@ -902,6 +906,7 @@ fn help_unknown_subcommand() {
----- stderr -----
error: There is no command `foo bar` for `uv`. Did you mean one of:
+ auth
run
init
add
@@ -959,6 +964,7 @@ fn help_with_global_option() {
Usage: uv [OPTIONS]
Commands:
+ auth Manage authentication
run Run a command or script
init Create a new project
add Add dependencies to the project
@@ -1081,6 +1087,7 @@ fn help_with_no_pager() {
Usage: uv [OPTIONS]
Commands:
+ auth Manage authentication
run Run a command or script
init Create a new project
add Add dependencies to the project
diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs
index 70e2de964..0e47d254a 100644
--- a/crates/uv/tests/it/main.rs
+++ b/crates/uv/tests/it/main.rs
@@ -3,6 +3,9 @@
pub(crate) mod common;
+#[cfg(feature = "keyring-tests")]
+mod auth;
+
mod branching_urls;
#[cfg(all(feature = "python", feature = "pypi"))]
diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs
index 487534b42..cc2a1ea50 100644
--- a/crates/uv/tests/it/show_settings.rs
+++ b/crates/uv/tests/it/show_settings.rs
@@ -7684,7 +7684,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
- PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT,
+ PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_KEYRING,
),
},
python_preference: Managed,
@@ -7908,7 +7908,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
- PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT,
+ PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_KEYRING,
),
},
python_preference: Managed,
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index 158f2cc1e..4e3c05d4a 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -12,7 +12,8 @@ uv [OPTIONS]
Commands
-uv runRun a command or script
+uv authManage authentication
+uv runRun a command or script
uv initCreate a new project
uv addAdd dependencies to the project
uv removeRemove dependencies from the project
@@ -33,6 +34,238 @@ uv [OPTIONS]
uv helpDisplay documentation for a command
+## uv auth
+
+Manage authentication
+
+Usage
+
+```
+uv auth [OPTIONS]
+```
+
+Commands
+
+uv auth loginLogin to a service
+uv auth logoutLogout of a service
+uv auth tokenShow the authentication token for a service
+
+
+### uv auth login
+
+Login to a service
+
+Usage
+
+```
+uv auth login [OPTIONS]
+```
+
+Arguments
+
+- SERVICE
The service to login to
+
+
+Options
+
+--allow-insecure-host, --trusted-host allow-insecure-hostAllow insecure connections to a host.
+Can be provided multiple times.
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+May also be set with the UV_INSECURE_HOST environment variable.
--cache-dir cache-dirPath to the cache directory.
+Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.
+To view the location of the cache directory, run uv cache dir.
+May also be set with the UV_CACHE_DIR environment variable.
--color color-choiceControl the use of color in output.
+By default, uv will automatically detect support for colors when writing to a terminal.
+Possible values:
+
+auto: Enables colored output only when the output is going to a terminal or TTY with support
+always: Enables colored output regardless of the detected environment
+never: Disables colored output
+
--config-file config-fileThe path to a uv.toml file to use for configuration.
+While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.
+May also be set with the UV_CONFIG_FILE environment variable.
--directory directoryChange to the given directory prior to running the command.
+Relative paths are resolved with the given directory as the base.
+See --project to only change the project root directory.
+--help, -hDisplay the concise help for this command
+--keyring-provider keyring-providerThe keyring provider to use for storage of credentials.
+Only --keyring-provider native is supported for login, which uses the system keyring via an integration built into uv.
+May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
+
+disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
+subprocess: Use the keyring command for credential lookup
+
--managed-pythonRequire use of uv-managed Python versions.
+By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
+May also be set with the UV_MANAGED_PYTHON environment variable.
--native-tlsWhether to load TLS certificates from the platform's native certificate store.
+By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).
+However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.
+May also be set with the UV_NATIVE_TLS environment variable.
--no-cache, --no-cache-dir, -nAvoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation
+May also be set with the UV_NO_CACHE environment variable.
--no-configAvoid discovering configuration files (pyproject.toml, uv.toml).
+Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.
+May also be set with the UV_NO_CONFIG environment variable.
--no-managed-pythonDisable use of uv-managed Python versions.
+Instead, uv will search for a suitable Python version on the system.
+May also be set with the UV_NO_MANAGED_PYTHON environment variable.
--no-progressHide all progress outputs.
+For example, spinners or progress bars.
+May also be set with the UV_NO_PROGRESS environment variable.
--no-python-downloadsDisable automatic downloads of Python.
+--offlineDisable network access.
+When disabled, uv will only use locally cached data and locally available files.
+May also be set with the UV_OFFLINE environment variable.
--password passwordThe password to use for the service
+--project projectRun the command within the given project directory.
+All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).
+Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
+See --directory to change the working directory entirely.
+This setting has no effect when used in the uv pip interface.
+May also be set with the UV_PROJECT environment variable.
--quiet, -qUse quiet output.
+Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.
+--token, -t tokenThe token to use for the service.
+The username will be set to __token__.
+--username, -u usernameThe username to use for the service
+--verbose, -vUse verbose output.
+You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)
+
+
+### uv auth logout
+
+Logout of a service
+
+Usage
+
+```
+uv auth logout [OPTIONS]
+```
+
+Arguments
+
+- SERVICE
The service to logout of
+
+
+Options
+
+--allow-insecure-host, --trusted-host allow-insecure-hostAllow insecure connections to a host.
+Can be provided multiple times.
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+May also be set with the UV_INSECURE_HOST environment variable.
--cache-dir cache-dirPath to the cache directory.
+Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.
+To view the location of the cache directory, run uv cache dir.
+May also be set with the UV_CACHE_DIR environment variable.
--color color-choiceControl the use of color in output.
+By default, uv will automatically detect support for colors when writing to a terminal.
+Possible values:
+
+auto: Enables colored output only when the output is going to a terminal or TTY with support
+always: Enables colored output regardless of the detected environment
+never: Disables colored output
+
--config-file config-fileThe path to a uv.toml file to use for configuration.
+While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.
+May also be set with the UV_CONFIG_FILE environment variable.
--directory directoryChange to the given directory prior to running the command.
+Relative paths are resolved with the given directory as the base.
+See --project to only change the project root directory.
+--help, -hDisplay the concise help for this command
+--keyring-provider keyring-providerThe keyring provider to use for storage of credentials.
+Only --keyring-provider native is supported for logout, which uses the system keyring via an integration built into uv.
+May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
+
+disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
+subprocess: Use the keyring command for credential lookup
+
--managed-pythonRequire use of uv-managed Python versions.
+By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
+May also be set with the UV_MANAGED_PYTHON environment variable.
--native-tlsWhether to load TLS certificates from the platform's native certificate store.
+By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).
+However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.
+May also be set with the UV_NATIVE_TLS environment variable.
--no-cache, --no-cache-dir, -nAvoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation
+May also be set with the UV_NO_CACHE environment variable.
--no-configAvoid discovering configuration files (pyproject.toml, uv.toml).
+Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.
+May also be set with the UV_NO_CONFIG environment variable.
--no-managed-pythonDisable use of uv-managed Python versions.
+Instead, uv will search for a suitable Python version on the system.
+May also be set with the UV_NO_MANAGED_PYTHON environment variable.
--no-progressHide all progress outputs.
+For example, spinners or progress bars.
+May also be set with the UV_NO_PROGRESS environment variable.
--no-python-downloadsDisable automatic downloads of Python.
+--offlineDisable network access.
+When disabled, uv will only use locally cached data and locally available files.
+May also be set with the UV_OFFLINE environment variable.
--project projectRun the command within the given project directory.
+All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).
+Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
+See --directory to change the working directory entirely.
+This setting has no effect when used in the uv pip interface.
+May also be set with the UV_PROJECT environment variable.
--quiet, -qUse quiet output.
+Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.
+--username, -u usernameThe username to logout
+--verbose, -vUse verbose output.
+You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)
+
+
+### uv auth token
+
+Show the authentication token for a service
+
+Usage
+
+```
+uv auth token [OPTIONS]
+```
+
+Arguments
+
+- SERVICE
The service to lookup
+
+
+Options
+
+--allow-insecure-host, --trusted-host allow-insecure-hostAllow insecure connections to a host.
+Can be provided multiple times.
+Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).
+WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.
+May also be set with the UV_INSECURE_HOST environment variable.
--cache-dir cache-dirPath to the cache directory.
+Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.
+To view the location of the cache directory, run uv cache dir.
+May also be set with the UV_CACHE_DIR environment variable.
--color color-choiceControl the use of color in output.
+By default, uv will automatically detect support for colors when writing to a terminal.
+Possible values:
+
+auto: Enables colored output only when the output is going to a terminal or TTY with support
+always: Enables colored output regardless of the detected environment
+never: Disables colored output
+
--config-file config-fileThe path to a uv.toml file to use for configuration.
+While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.
+May also be set with the UV_CONFIG_FILE environment variable.
--directory directoryChange to the given directory prior to running the command.
+Relative paths are resolved with the given directory as the base.
+See --project to only change the project root directory.
+--help, -hDisplay the concise help for this command
+--keyring-provider keyring-providerThe keyring provider to use for reading credentials
+May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
+
+disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
+subprocess: Use the keyring command for credential lookup
+
--managed-pythonRequire use of uv-managed Python versions.
+By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
+May also be set with the UV_MANAGED_PYTHON environment variable.
--native-tlsWhether to load TLS certificates from the platform's native certificate store.
+By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).
+However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.
+May also be set with the UV_NATIVE_TLS environment variable.
--no-cache, --no-cache-dir, -nAvoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation
+May also be set with the UV_NO_CACHE environment variable.
--no-configAvoid discovering configuration files (pyproject.toml, uv.toml).
+Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.
+May also be set with the UV_NO_CONFIG environment variable.
--no-managed-pythonDisable use of uv-managed Python versions.
+Instead, uv will search for a suitable Python version on the system.
+May also be set with the UV_NO_MANAGED_PYTHON environment variable.
--no-progressHide all progress outputs.
+For example, spinners or progress bars.
+May also be set with the UV_NO_PROGRESS environment variable.
--no-python-downloadsDisable automatic downloads of Python.
+--offlineDisable network access.
+When disabled, uv will only use locally cached data and locally available files.
+May also be set with the UV_OFFLINE environment variable.
--project projectRun the command within the given project directory.
+All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).
+Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
+See --directory to change the working directory entirely.
+This setting has no effect when used in the uv pip interface.
+May also be set with the UV_PROJECT environment variable.
--quiet, -qUse quiet output.
+Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.
+--username, -u usernameThe username to lookup
+--verbose, -vUse verbose output.
+You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)
+
+
## uv run
Run a command or script.
@@ -149,6 +382,7 @@ uv run [OPTIONS] [COMMAND]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -554,6 +788,7 @@ uv add [OPTIONS] >
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -754,6 +989,7 @@ uv remove [OPTIONS] ...
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -936,6 +1172,7 @@ uv version [OPTIONS] [VALUE]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -1134,6 +1371,7 @@ uv sync [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -1379,6 +1617,7 @@ uv lock [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -1558,6 +1797,7 @@ uv export [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -1752,6 +1992,7 @@ uv tree [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -2095,6 +2336,7 @@ uv tool run [OPTIONS] [COMMAND]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -2316,6 +2558,7 @@ uv tool install [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -2529,6 +2772,7 @@ uv tool upgrade [OPTIONS] ...
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -3749,6 +3993,7 @@ uv pip compile [OPTIONS] >
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -4044,6 +4289,7 @@ uv pip sync [OPTIONS] ...
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -4317,6 +4563,7 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.
@@ -4563,6 +4810,7 @@ uv pip uninstall [OPTIONS] >
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--managed-pythonRequire use of uv-managed Python versions.
By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
@@ -4741,6 +4989,7 @@ uv pip list [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--managed-pythonRequire use of uv-managed Python versions.
By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
@@ -4916,6 +5165,7 @@ uv pip tree [OPTIONS]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--managed-pythonRequire use of uv-managed Python versions.
By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
@@ -5157,6 +5407,7 @@ uv venv [OPTIONS] [PATH]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used for installing seed packages.
@@ -5313,6 +5564,7 @@ uv build [OPTIONS] [SRC]
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -5465,6 +5717,7 @@ uv publish --publish-url https://upload.pypi.org/legacy/ --check-url https://pyp
May also be set with the UV_KEYRING_PROVIDER environment variable.
Possible values:
disabled: Do not use keyring for credential lookup
+native: Use a native integration with the system keychain for credential lookup
subprocess: Use the keyring command for credential lookup
--managed-pythonRequire use of uv-managed Python versions.
By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.
diff --git a/uv.schema.json b/uv.schema.json
index 655ce6a84..e062fca5b 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -1144,6 +1144,11 @@
"type": "string",
"const": "disabled"
},
+ {
+ "description": "Use a native integration with the system keychain for credential lookup.",
+ "type": "string",
+ "const": "native"
+ },
{
"description": "Use the `keyring` command for credential lookup.",
"type": "string",