Skip to main content

cargo/util/auth/
mod.rs

1//! Registry authentication support.
2
3use crate::{
4    context::ConfigKey,
5    util::{CanonicalUrl, CargoResult, GlobalContext, IntoUrl},
6    workspace::features::cargo_docs_link,
7};
8use anyhow::{Context as _, bail};
9use cargo_credential::{
10    Action, CacheControl, Credential, CredentialResponse, LoginOptions, Operation, RegistryInfo,
11    Secret,
12};
13
14use core::fmt;
15use std::error::Error;
16use time::{Duration, OffsetDateTime};
17use url::Url;
18
19use crate::context::Value;
20use crate::util::credential::adaptor::BasicProcessCredential;
21use crate::workspace::SourceId;
22
23use super::{credential::process::CredentialProcessCredential, credential::token::TokenCredential};
24use crate::context::{CredentialCacheValue, GlobalRegistryConfig, PathAndArgs, RegistryConfig};
25
26/// Get the list of credential providers for a registry source.
27fn credential_provider(
28    gctx: &GlobalContext,
29    sid: &SourceId,
30    require_cred_provider_config: bool,
31    show_warnings: bool,
32) -> CargoResult<Vec<Vec<String>>> {
33    let warn = |message: String| {
34        if show_warnings {
35            gctx.shell().warn(message)
36        } else {
37            Ok(())
38        }
39    };
40
41    let cfg = registry_credential_config_raw(gctx, sid)?;
42    let mut global_provider_defined = true;
43    let default_providers = || {
44        global_provider_defined = false;
45        vec![vec!["cargo:token".to_string()]]
46    };
47    let global_providers = gctx
48        .get::<Option<Vec<Value<String>>>>(["registry", "global-credential-providers"])?
49        .filter(|p| !p.is_empty())
50        .map(|p| {
51            p.iter()
52                .rev()
53                .map(PathAndArgs::from_whitespace_separated_string)
54                .map(|p| resolve_credential_alias(gctx, p))
55                .collect()
56        })
57        .unwrap_or_else(default_providers);
58    tracing::debug!(?global_providers);
59
60    match cfg {
61        // If there's a specific provider configured for this registry, use it.
62        Some(RegistryConfig {
63            credential_provider: Some(provider),
64            token,
65            ..
66        }) => {
67            let provider = resolve_credential_alias(gctx, provider);
68            if let Some(token) = token {
69                if provider[0] != "cargo:token" {
70                    warn(format!(
71                        "{sid} has a token configured in {} that will be ignored \
72                        because this registry is configured to use credential-provider `{}`",
73                        token.definition, provider[0],
74                    ))?;
75                }
76            }
77            return Ok(vec![provider]);
78        }
79
80        // Check if a `token` is configured that will be ignored.
81        Some(RegistryConfig {
82            token: Some(token), ..
83        }) => {
84            if !global_providers
85                .iter()
86                .any(|p| p.first().map(String::as_str) == Some("cargo:token"))
87            {
88                warn(format!(
89                    "{sid} has a token configured in {} that will be ignored \
90                    because the `cargo:token` credential provider is not listed in \
91                    `registry.global-credential-providers`",
92                    token.definition
93                ))?;
94            }
95        }
96
97        // If we couldn't find a registry-specific provider, use the fallback provider list.
98        None | Some(RegistryConfig { .. }) => {}
99    };
100    if !global_provider_defined && require_cred_provider_config {
101        bail!(
102            "authenticated registries require a credential-provider to be configured\n\
103        see {} for details",
104            cargo_docs_link("reference/registry-authentication.html")
105        );
106    }
107    Ok(global_providers)
108}
109
110/// Get the credential configuration for a `SourceId`.
111pub fn registry_credential_config_raw(
112    gctx: &GlobalContext,
113    sid: &SourceId,
114) -> CargoResult<Option<RegistryConfig>> {
115    let mut cache = gctx.registry_config();
116    if let Some(cfg) = cache.get(&sid) {
117        return Ok(cfg.clone());
118    }
119    let cfg = registry_credential_config_raw_uncached(gctx, sid)?;
120    cache.insert(*sid, cfg.clone());
121    return Ok(cfg);
122}
123
124fn registry_credential_config_raw_uncached(
125    gctx: &GlobalContext,
126    sid: &SourceId,
127) -> CargoResult<Option<RegistryConfig>> {
128    tracing::trace!("loading credential config for {}", sid);
129    gctx.load_credentials()?;
130    if !sid.is_remote_registry() {
131        bail!(
132            "{} does not support API commands.\n\
133             Check for a source-replacement in .cargo/config.",
134            sid
135        );
136    }
137
138    // Handle crates.io specially, since it uses different configuration keys.
139    if sid.is_crates_io() {
140        gctx.check_registry_index_not_set()?;
141        return Ok(gctx
142            .get::<Option<GlobalRegistryConfig>>(["registry"])?
143            .map(|c| c.to_registry_config()));
144    }
145
146    // Find the SourceId's name by its index URL. If environment variables
147    // are available they will be preferred over configuration values.
148    //
149    // The fundamental problem is that we only know the index url of the registry
150    // for certain. For example, an unnamed registry source can come from the `--index`
151    // command line argument, or from a Cargo.lock file. For this reason, we always
152    // attempt to discover the name by looking it up by the index URL.
153    //
154    // This also allows the authorization token for a registry to be set
155    // without knowing the registry name by using the _INDEX and _TOKEN
156    // environment variables.
157
158    let name = {
159        // Discover names from environment variables.
160        let index = sid.canonical_url();
161        let mut names: Vec<_> = gctx
162            .env()
163            .filter_map(|(k, v)| {
164                Some((
165                    k.strip_prefix("CARGO_REGISTRIES_")?
166                        .strip_suffix("_INDEX")?,
167                    v,
168                ))
169            })
170            .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?)))
171            .filter(|(_, v)| v == index)
172            .map(|(k, _)| k.to_lowercase())
173            .collect();
174
175        // Discover names from the configuration only if none were found in the environment.
176        if names.len() == 0 {
177            if let Some(registries) = gctx.values()?.get("registries") {
178                let (registries, _) = registries.table("registries")?;
179                for (name, value) in registries {
180                    if let Some(v) = value.table(&format!("registries.{name}"))?.0.get("index") {
181                        let (v, _) = v.string(&format!("registries.{name}.index"))?;
182                        if index == &CanonicalUrl::new(&v.into_url()?)? {
183                            names.push(name.clone());
184                        }
185                    }
186                }
187            }
188        }
189        names.sort();
190        match names.len() {
191            0 => None,
192            1 => Some(std::mem::take(&mut names[0])),
193            _ => anyhow::bail!(
194                "multiple registries are configured with the same index url '{}': {}",
195                &sid.as_url(),
196                names.join(", ")
197            ),
198        }
199    };
200
201    // It's possible to have a registry configured in a Cargo config file,
202    // then override it with configuration from environment variables.
203    // If the name doesn't match, leave a note to help the user understand
204    // the potentially confusing situation.
205    if let Some(name) = name.as_deref() {
206        if Some(name) != sid.alt_registry_key() {
207            gctx.shell().note(format!(
208                "name of alternative registry `{}` set to `{name}`",
209                sid.url()
210            ))?
211        }
212    }
213
214    if let Some(name) = &name {
215        tracing::debug!("found alternative registry name `{name}` for {sid}");
216        gctx.get::<Option<RegistryConfig>>(["registries", name.as_str()])
217    } else {
218        tracing::debug!("no registry name found for {sid}");
219        Ok(None)
220    }
221}
222
223/// Use the `[credential-alias]` table to see if the provider name has been aliased.
224fn resolve_credential_alias(gctx: &GlobalContext, mut provider: PathAndArgs) -> Vec<String> {
225    if provider.args.is_empty() {
226        let name = provider.path.raw_value();
227        let key = format!("credential-alias.{name}");
228        if let Ok(alias) = gctx.get::<Value<PathAndArgs>>(["credential-alias", name]) {
229            tracing::debug!("resolving credential alias '{key}' -> '{alias:?}'");
230            if BUILT_IN_PROVIDERS.contains(&name) {
231                let _ = gctx.shell().warn(format!(
232                    "credential-alias `{name}` (defined in `{}`) will be \
233                    ignored because it would shadow a built-in credential-provider",
234                    alias.definition
235                ));
236            } else {
237                provider = alias.val;
238            }
239        }
240    }
241    provider.args.insert(
242        0,
243        provider
244            .path
245            .resolve_program(gctx)
246            .to_str()
247            .unwrap()
248            .to_string(),
249    );
250    provider.args
251}
252
253#[derive(Debug, PartialEq)]
254pub enum AuthorizationErrorReason {
255    TokenMissing,
256    TokenRejected,
257}
258
259impl fmt::Display for AuthorizationErrorReason {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        match self {
262            AuthorizationErrorReason::TokenMissing => write!(f, "no token found"),
263            AuthorizationErrorReason::TokenRejected => write!(f, "token rejected"),
264        }
265    }
266}
267
268/// An authorization error from accessing a registry.
269#[derive(Debug)]
270pub struct AuthorizationError {
271    /// Url that was attempted
272    sid: SourceId,
273    /// The `registry.default` config value.
274    default_registry: Option<String>,
275    /// Url where the user could log in.
276    pub login_url: Option<Url>,
277    /// Specific reason indicating what failed
278    reason: AuthorizationErrorReason,
279    /// Should `cargo login` and the `_TOKEN` env var be included when displaying this error?
280    supports_cargo_token_credential_provider: bool,
281    /// Whether the cached token appears to lack an authentication scheme (no space found).
282    token_lacks_scheme: Option<bool>,
283}
284
285impl AuthorizationError {
286    pub fn new(
287        gctx: &GlobalContext,
288        sid: SourceId,
289        login_url: Option<Url>,
290        reason: AuthorizationErrorReason,
291    ) -> CargoResult<Self> {
292        // Only display the _TOKEN environment variable suggestion if the `cargo:token` credential
293        // provider is available for the source. Otherwise setting the environment variable will
294        // have no effect.
295        let supports_cargo_token_credential_provider =
296            credential_provider(gctx, &sid, false, false)?
297                .iter()
298                .any(|p| p.first().map(String::as_str) == Some("cargo:token"));
299        let cache = gctx.credential_cache();
300        let token_lacks_scheme = cache
301            .get(sid.canonical_url())
302            .map(|entry| !entry.token_value.as_deref().expose().contains(' '));
303        Ok(AuthorizationError {
304            sid,
305            default_registry: gctx.default_registry()?,
306            login_url,
307            reason,
308            supports_cargo_token_credential_provider,
309            token_lacks_scheme,
310        })
311    }
312}
313
314impl Error for AuthorizationError {}
315impl fmt::Display for AuthorizationError {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        if self.sid.is_crates_io() {
318            let args = if self.default_registry.is_some() {
319                " --registry crates-io"
320            } else {
321                ""
322            };
323            write!(f, "{}, please run `cargo login{args}`", self.reason)?;
324            if self.supports_cargo_token_credential_provider {
325                write!(f, "\nor use environment variable CARGO_REGISTRY_TOKEN")?;
326            }
327            Ok(())
328        } else if let Some(name) = self.sid.alt_registry_key() {
329            write!(
330                f,
331                "{} for `{}`",
332                self.reason,
333                self.sid.display_registry_name()
334            )?;
335            if self.supports_cargo_token_credential_provider {
336                let key = ConfigKey::from_str(&format!("registries.{name}.token"));
337                write!(
338                    f,
339                    ", please run `cargo login --registry {name}`\n\
340                    or use environment variable {}",
341                    key.as_env_key()
342                )?;
343            } else {
344                write!(
345                    f,
346                    "\nYou may need to log in using this registry's credential provider"
347                )?;
348            }
349
350            if self.reason == AuthorizationErrorReason::TokenRejected {
351                if self.token_lacks_scheme == Some(true) {
352                    write!(
353                        f,
354                        "\nnote: the token does not include an authentication scheme"
355                    )?;
356                }
357            }
358            Ok(())
359        } else if self.reason == AuthorizationErrorReason::TokenMissing {
360            write!(
361                f,
362                r#"{} for `{}`
363consider setting up an alternate registry in Cargo's configuration
364as described by https://doc.rust-lang.org/cargo/reference/registries.html
365
366[registries]
367my-registry = {{ index = "{}" }}
368"#,
369                self.reason,
370                self.sid.display_registry_name(),
371                self.sid.url()
372            )
373        } else {
374            write!(
375                f,
376                r#"{} for `{}`"#,
377                self.reason,
378                self.sid.display_registry_name(),
379            )
380        }
381    }
382}
383
384/// Store a token in the cache for future calls.
385pub fn cache_token_from_commandline(gctx: &GlobalContext, sid: &SourceId, token: Secret<&str>) {
386    let url = sid.canonical_url();
387    gctx.credential_cache().insert(
388        url.clone(),
389        CredentialCacheValue {
390            token_value: token.to_owned(),
391            expiration: None,
392            operation_independent: true,
393        },
394    );
395}
396
397/// List of credential providers built-in to Cargo.
398/// Keep in sync with the `match` in `credential_action`.
399static BUILT_IN_PROVIDERS: &[&'static str] = &[
400    "cargo:token",
401    "cargo:token-from-stdout",
402    "cargo:wincred",
403    "cargo:macos-keychain",
404    "cargo:libsecret",
405];
406
407/// Retrieves a cached instance of `LibSecretCredential`.
408/// Must be cached to avoid repeated load/unload cycles, which are not supported by `glib`.
409#[cfg(target_os = "linux")]
410fn get_credential_libsecret()
411-> CargoResult<&'static cargo_credential_libsecret::LibSecretCredential> {
412    static CARGO_CREDENTIAL_LIBSECRET: std::sync::OnceLock<
413        cargo_credential_libsecret::LibSecretCredential,
414    > = std::sync::OnceLock::new();
415    // Unfortunately `get_or_try_init` is not yet stable. This workaround is not threadsafe but
416    // loading libsecret twice will only temporary increment the ref counter, which is decrement
417    // again when `drop` is called.
418    match CARGO_CREDENTIAL_LIBSECRET.get() {
419        Some(lib) => Ok(lib),
420        None => {
421            let _ = CARGO_CREDENTIAL_LIBSECRET
422                .set(cargo_credential_libsecret::LibSecretCredential::new()?);
423            Ok(CARGO_CREDENTIAL_LIBSECRET.get().unwrap())
424        }
425    }
426}
427
428fn credential_action(
429    gctx: &GlobalContext,
430    sid: &SourceId,
431    action: Action<'_>,
432    headers: Vec<String>,
433    args: &[&str],
434    require_cred_provider_config: bool,
435) -> CargoResult<CredentialResponse> {
436    let name = sid.alt_registry_key();
437    let registry = RegistryInfo {
438        index_url: sid.url().as_str(),
439        name,
440        headers,
441    };
442    let providers = credential_provider(gctx, sid, require_cred_provider_config, true)?;
443    let mut any_not_found = false;
444    for provider in providers {
445        let args: Vec<&str> = provider
446            .iter()
447            .map(String::as_str)
448            .chain(args.iter().copied())
449            .collect();
450        let process = args[0];
451        tracing::debug!("attempting credential provider: {args:?}");
452        // If the available built-in providers are changed, update the `BUILT_IN_PROVIDERS` list.
453        let provider: Box<dyn Credential> = match process {
454            "cargo:token" => Box::new(TokenCredential::new(gctx)),
455            "cargo:token-from-stdout" => Box::new(BasicProcessCredential {}),
456            #[cfg(windows)]
457            "cargo:wincred" => Box::new(cargo_credential_wincred::WindowsCredential {}),
458            #[cfg(target_os = "macos")]
459            "cargo:macos-keychain" => Box::new(cargo_credential_macos_keychain::MacKeychain {}),
460            #[cfg(target_os = "linux")]
461            "cargo:libsecret" => Box::new(get_credential_libsecret()?),
462            name if BUILT_IN_PROVIDERS.contains(&name) => {
463                Box::new(cargo_credential::UnsupportedCredential {})
464            }
465            process => Box::new(CredentialProcessCredential::new(process)),
466        };
467        gctx.shell().verbose(|c| {
468            c.status(
469                "Credential",
470                format!(
471                    "{} {action} {}",
472                    args.join(" "),
473                    sid.display_registry_name()
474                ),
475            )
476        })?;
477        match provider.perform(&registry, &action, &args[1..]) {
478            Ok(response) => return Ok(response),
479            Err(cargo_credential::Error::UrlNotSupported) => {}
480            Err(cargo_credential::Error::NotFound) => any_not_found = true,
481            e => {
482                return e.with_context(|| {
483                    format!(
484                        "credential provider `{}` failed action `{action}`",
485                        args.join(" ")
486                    )
487                });
488            }
489        }
490    }
491    if any_not_found {
492        Err(cargo_credential::Error::NotFound.into())
493    } else {
494        anyhow::bail!("no credential providers could handle the request")
495    }
496}
497
498/// Returns the token to use for the given registry.
499/// If a `login_url` is provided and a token is not available, the
500/// `login_url` will be included in the returned error.
501pub fn auth_token(
502    gctx: &GlobalContext,
503    sid: &SourceId,
504    login_url: Option<&Url>,
505    operation: Operation<'_>,
506    headers: Vec<String>,
507    require_cred_provider_config: bool,
508) -> CargoResult<String> {
509    match auth_token_optional(gctx, sid, operation, headers, require_cred_provider_config)? {
510        Some(token) => Ok(token.expose()),
511        None => Err(AuthorizationError::new(
512            gctx,
513            *sid,
514            login_url.cloned(),
515            AuthorizationErrorReason::TokenMissing,
516        )?
517        .into()),
518    }
519}
520
521/// Returns the token to use for the given registry.
522fn auth_token_optional(
523    gctx: &GlobalContext,
524    sid: &SourceId,
525    operation: Operation<'_>,
526    headers: Vec<String>,
527    require_cred_provider_config: bool,
528) -> CargoResult<Option<Secret<String>>> {
529    tracing::trace!("token requested for {}", sid.display_registry_name());
530    let mut cache = gctx.credential_cache();
531    let url = sid.canonical_url();
532    if let Some(cached_token) = cache.get(url) {
533        if cached_token
534            .expiration
535            .map(|exp| OffsetDateTime::now_utc() + Duration::minutes(1) < exp)
536            .unwrap_or(true)
537        {
538            if cached_token.operation_independent || matches!(operation, Operation::Read) {
539                tracing::trace!("using token from in-memory cache");
540                return Ok(Some(cached_token.token_value.clone()));
541            }
542        } else {
543            // Remove expired token from the cache
544            cache.remove(url);
545        }
546    }
547
548    let credential_response = credential_action(
549        gctx,
550        sid,
551        Action::Get(operation),
552        headers,
553        &[],
554        require_cred_provider_config,
555    );
556    if let Some(e) = credential_response.as_ref().err() {
557        if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
558            if matches!(e, cargo_credential::Error::NotFound) {
559                return Ok(None);
560            }
561        }
562    }
563    let credential_response = credential_response?;
564
565    let CredentialResponse::Get {
566        token,
567        cache: cache_control,
568        operation_independent,
569    } = credential_response
570    else {
571        bail!(
572            "credential provider produced unexpected response for `get` request: {credential_response:?}"
573        )
574    };
575    let token = Secret::from(token);
576    tracing::trace!("found token");
577    let expiration = match cache_control {
578        CacheControl::Expires { expiration } => Some(expiration),
579        CacheControl::Session => None,
580        CacheControl::Never | _ => return Ok(Some(token)),
581    };
582
583    cache.insert(
584        url.clone(),
585        CredentialCacheValue {
586            token_value: token.clone(),
587            expiration,
588            operation_independent,
589        },
590    );
591    Ok(Some(token))
592}
593
594/// Log out from the given registry.
595pub fn logout(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<()> {
596    let credential_response = credential_action(gctx, sid, Action::Logout, vec![], &[], false);
597    if let Some(e) = credential_response.as_ref().err() {
598        if let Some(e) = e.downcast_ref::<cargo_credential::Error>() {
599            if matches!(e, cargo_credential::Error::NotFound) {
600                gctx.shell().status(
601                    "Logout",
602                    format!(
603                        "not currently logged in to `{}`",
604                        sid.display_registry_name()
605                    ),
606                )?;
607                return Ok(());
608            }
609        }
610    }
611    let credential_response = credential_response?;
612    let CredentialResponse::Logout = credential_response else {
613        bail!(
614            "credential provider produced unexpected response for `logout` request: {credential_response:?}"
615        )
616    };
617    Ok(())
618}
619
620/// Log in to the given registry.
621pub fn login(
622    gctx: &GlobalContext,
623    sid: &SourceId,
624    options: LoginOptions<'_>,
625    args: &[&str],
626) -> CargoResult<()> {
627    let credential_response =
628        credential_action(gctx, sid, Action::Login(options), vec![], args, false)?;
629    let CredentialResponse::Login = credential_response else {
630        bail!(
631            "credential provider produced unexpected response for `login` request: {credential_response:?}"
632        )
633    };
634    Ok(())
635}