Skip to main content

cargo/util/auth/
mod.rs

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