Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions crates/cargo-test-support/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,10 @@ type RequestCallback = Box<dyn Send + Fn(&Request, &HttpServer) -> Response>;
pub struct RegistryBuilder {
/// If set, configures an alternate registry with the given name.
alternative: Option<String>,
/// The authorization token for the registry.
/// The client-supplied authorization token for the registry.
token: Option<Token>,
/// The actual server authorization token for the registry.
server_token: Option<Token>,
/// If set, the registry requires authorization for all operations.
auth_required: bool,
/// If set, serves the index over http.
Expand Down Expand Up @@ -241,6 +243,7 @@ impl RegistryBuilder {
RegistryBuilder {
alternative: None,
token: None,
server_token: None,
auth_required: false,
http_api: false,
http_index: false,
Expand Down Expand Up @@ -309,13 +312,20 @@ impl RegistryBuilder {
self
}

/// Sets the token value
/// Sets the client-supplied token value
#[must_use]
pub fn token(mut self, token: Token) -> Self {
self.token = Some(token);
self
}

/// Sets the server token value
#[must_use]
pub fn server_token(mut self, token: Token) -> Self {
self.server_token = Some(token);
self
}

/// Sets this registry to require the authentication token for
/// all operations.
#[must_use]
Expand Down Expand Up @@ -372,6 +382,9 @@ impl RegistryBuilder {
.token
.unwrap_or_else(|| Token::Plaintext(format!("{prefix}sekrit")));

// Uses the client token unless otherwise set.
let server_token = self.server_token.unwrap_or_else(|| token.clone());

let (server, index_url, api_url, dl_url) = if !self.http_index && !self.http_api {
// No need to start the HTTP server.
(None, index_url, api_url, dl_url)
Expand All @@ -380,7 +393,7 @@ impl RegistryBuilder {
registry_path.clone(),
dl_path,
api_path.clone(),
token.clone(),
server_token.clone(),
self.auth_required,
self.custom_responders,
self.not_found_handler,
Expand Down
13 changes: 13 additions & 0 deletions src/cargo/sources/registry/http_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub struct HttpRegistry<'gctx> {
/// Should we include the authorization header?
auth_required: bool,

/// The scheme of the included authorization header, if any.
authorization_scheme: Option<auth::AuthorizationScheme>,

/// Url to get a token for the registry.
login_url: Option<Url>,

Expand Down Expand Up @@ -231,6 +234,7 @@ impl<'gctx> HttpRegistry<'gctx> {
fetch_started: false,
registry_config: None,
auth_required: false,
authorization_scheme: None,
login_url: None,
auth_error_headers: vec![],
quiet: false,
Expand Down Expand Up @@ -604,6 +608,7 @@ impl<'gctx> RegistryData for HttpRegistry<'gctx> {
self.source_id,
self.login_url.clone(),
auth::AuthorizationErrorReason::TokenRejected,
self.authorization_scheme.clone(),
)?;
return Poll::Ready(err.context(auth_error));
} else {
Expand Down Expand Up @@ -663,6 +668,14 @@ impl<'gctx> RegistryData for HttpRegistry<'gctx> {
self.auth_error_headers.clone(),
true,
)?;
let (scheme, _token) = authorization
.split_once(" ")
.unwrap_or(("", &authorization));
self.authorization_scheme = match scheme.to_ascii_lowercase().as_str() {
"basic" => Some(auth::AuthorizationScheme::Basic),
"bearer" => Some(auth::AuthorizationScheme::Bearer),
_ => Some(auth::AuthorizationScheme::Unrecognized),
};
headers.append(&format!("Authorization: {}", authorization))?;
trace!(target: "network", "including authorization for {}", full_url);
}
Expand Down
35 changes: 35 additions & 0 deletions src/cargo/util/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,23 @@ impl fmt::Display for AuthorizationErrorReason {
}
}

#[derive(Clone, Debug, PartialEq)]
pub enum AuthorizationScheme {
Basic,
Bearer,
Unrecognized,
}

impl fmt::Display for AuthorizationScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthorizationScheme::Basic => write!(f, "Basic"),
AuthorizationScheme::Bearer => write!(f, "Bearer"),
AuthorizationScheme::Unrecognized => write!(f, "unrecognized"),
}
}
}

/// An authorization error from accessing a registry.
#[derive(Debug)]
pub struct AuthorizationError {
Expand All @@ -400,6 +417,8 @@ pub struct AuthorizationError {
reason: AuthorizationErrorReason,
/// Should `cargo login` and the `_TOKEN` env var be included when displaying this error?
supports_cargo_token_credential_provider: bool,
/// What authorization scheme was specified in the token, if any?
scheme: Option<AuthorizationScheme>,
}

impl AuthorizationError {
Expand All @@ -408,6 +427,7 @@ impl AuthorizationError {
sid: SourceId,
login_url: Option<Url>,
reason: AuthorizationErrorReason,
scheme: Option<AuthorizationScheme>,
) -> CargoResult<Self> {
// Only display the _TOKEN environment variable suggestion if the `cargo:token` credential
// provider is available for the source. Otherwise setting the environment variable will
Expand All @@ -422,6 +442,7 @@ impl AuthorizationError {
login_url,
reason,
supports_cargo_token_credential_provider,
scheme,
})
}
}
Expand Down Expand Up @@ -461,6 +482,19 @@ impl fmt::Display for AuthorizationError {
"\nYou may need to log in using this registry's credential provider"
)?;
}
if let Some(scheme) = &self.scheme {
write!(
f,
"Your registry token is prefixed with an embedded {} authentication scheme. Is this correct?",
scheme,
)?;
} else {
write!(
f,
"Your registry token is not prefixed with an embedded authorization scheme (e.g. `Bearer `).\n\

Choose a reason for hiding this comment

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

🎉 This messaging would have saved me on the first hit :)

Does this registry require an authentication scheme?",
)?;
}
Ok(())
} else if self.reason == AuthorizationErrorReason::TokenMissing {
write!(
Expand Down Expand Up @@ -624,6 +658,7 @@ pub fn auth_token(
*sid,
login_url.cloned(),
AuthorizationErrorReason::TokenMissing,
None,
)?
.into()),
}
Expand Down
195 changes: 195 additions & 0 deletions tests/testsuite/alt_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use cargo_test_support::publish::validate_alt_upload;
use cargo_test_support::registry::{self, Package, RegistryBuilder};
use cargo_test_support::str;
use cargo_test_support::{basic_manifest, paths, project};
use rand::Rng;

#[cargo_test]
fn depend_on_alt_registry() {
Expand Down Expand Up @@ -1985,3 +1986,197 @@ fn empty_dependency_registry() {
"#]])
.run();
}

enum TokenStatus {
Valid,
Missing,
InvalidHasScheme,
InvalidNoScheme,
}

struct TokenTest {
crates_io_token: TokenStatus,
alternative_token: TokenStatus,
expected_message: &'static str,
}

fn token_test(token_test: TokenTest) {
let server_token = rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(8)
.map(char::from)
.collect::<String>();

let crates_io_builder = RegistryBuilder::new()
.http_api()
.http_index()
.credential_provider(&[&"cargo:token"])
.auth_required();

let alternative_builder = RegistryBuilder::new()
.http_api()
.http_index()
.credential_provider(&[&"cargo:token"])
.alternative()
.auth_required();

let crates_io_mock = match token_test.crates_io_token {
TokenStatus::Valid => crates_io_builder.build(),
TokenStatus::Missing => crates_io_builder.no_configure_token().build(),
TokenStatus::InvalidHasScheme => crates_io_builder
.token(cargo_test_support::registry::Token::Plaintext(
"Bearer <TOKEN>".to_string(),
))
.server_token(cargo_test_support::registry::Token::Plaintext(
server_token.clone(),
))
.build(),
TokenStatus::InvalidNoScheme => crates_io_builder
.token(cargo_test_support::registry::Token::Plaintext(
"<TOKEN>".to_string(),
))
.server_token(cargo_test_support::registry::Token::Plaintext(
server_token.clone(),
))
.build(),
};
let _alternative_mock = match token_test.alternative_token {
TokenStatus::Valid => alternative_builder.build(),
TokenStatus::Missing => alternative_builder.no_configure_token().build(),
TokenStatus::InvalidHasScheme => alternative_builder
.token(cargo_test_support::registry::Token::Plaintext(
"Bearer <TOKEN>".to_string(),
))
.server_token(cargo_test_support::registry::Token::Plaintext(
server_token.clone(),
))
.build(),
TokenStatus::InvalidNoScheme => alternative_builder
.token(cargo_test_support::registry::Token::Plaintext(
"<TOKEN>".to_string(),
))
.server_token(cargo_test_support::registry::Token::Plaintext(
server_token.clone(),
))
.build(),
};

let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.0.1"
authors = []
edition = "2015"

[dependencies.bar_alt]
version = "0.0.1"
registry = "alternative"

[dependencies.bar_crates]
version = "0.0.1"
"#,
)
.file("src/main.rs", "fn main() {}")
.build();

Package::new("bar_alt", "0.0.1").alternative(true).publish();
Package::new("bar_crates", "0.0.1").publish();
p.cargo("build")
.replace_crates_io(crates_io_mock.index_url())
.with_status(101)
.with_stderr_contains(token_test.expected_message)
.run();
}

macro_rules! token_error_messages {
($($name:ident: $value:expr)*) => {
$(
#[cargo_test]
fn $name() {
token_test($value);
}
)*
}
}

token_error_messages! {
Copy link
Author

@nathanhammond nathanhammond Sep 24, 2025

Choose a reason for hiding this comment

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

@epage: since this whole PR is basically test cases followed by a tiny bit of string manipulation, would you mind taking an incremental look at just the test commit?

(the tests pass, auth_required on crates.io I've not yet attempted to discover if publish --workspace can support both crates.io and custom registries simultaneously. Regardless, I started with the cross-product to fully enumerate behavior.)

// Skips crates_valid_alt_valid because it would not error.
crates_valid_alt_missing: TokenTest {
crates_io_token: TokenStatus::Valid,
alternative_token: TokenStatus::Missing,
expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN",
}
crates_valid_alt_invalid_has_scheme: TokenTest {
crates_io_token: TokenStatus::Valid,
alternative_token: TokenStatus::InvalidHasScheme,
expected_message: " token rejected for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN",
}
crates_valid_alt_invalid_no_scheme: TokenTest {
crates_io_token: TokenStatus::Valid,
alternative_token: TokenStatus::InvalidNoScheme,
expected_message: " token rejected for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN",
}
crates_missing_alt_valid: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::Valid,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_missing_alt_missing: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::Missing,
expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN",
}
crates_missing_alt_invalid_has_scheme: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::InvalidHasScheme,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_missing_alt_invalid_no_scheme: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::InvalidNoScheme,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_invalid_has_scheme_alt_valid: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::Valid,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_invalid_has_scheme_alt_missing: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::Missing,
expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN",
}
crates_invalid_has_scheme_alt_invalid_has_scheme: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::InvalidHasScheme,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_invalid_has_scheme_alt_invalid_no_scheme: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::InvalidNoScheme,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_invalid_no_scheme_alt_valid: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::Valid,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_invalid_no_scheme_alt_missing: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::Missing,
expected_message: " no token found for `alternative`, please run `cargo login --registry alternative`\n or use environment variable CARGO_REGISTRIES_ALTERNATIVE_TOKEN",
}
crates_invalid_no_scheme_alt_invalid_has_scheme: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::InvalidHasScheme,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
crates_invalid_no_scheme_alt_invalid_no_scheme: TokenTest {
crates_io_token: TokenStatus::Missing,
alternative_token: TokenStatus::InvalidNoScheme,
expected_message: " no token found, please run `cargo login`\n or use environment variable CARGO_REGISTRY_TOKEN",
}
}
Loading