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