#
cargo/util/auth/
mod.rs

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