Skip to main content

cargo/util/credential/
token.rs

1//! Credential provider that uses plaintext tokens in Cargo's config.
2
3use anyhow::Context as _;
4use cargo_credential::{Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo};
5use url::Url;
6
7use crate::{
8    GlobalContext, context, ops::RegistryCredentialConfig,
9    util::auth::registry_credential_config_raw, workspace::SourceId,
10};
11
12pub struct TokenCredential<'a> {
13    gctx: &'a GlobalContext,
14}
15
16impl<'a> TokenCredential<'a> {
17    pub fn new(gctx: &'a GlobalContext) -> Self {
18        Self { gctx }
19    }
20}
21
22impl<'a> Credential for TokenCredential<'a> {
23    fn perform(
24        &self,
25        registry: &RegistryInfo<'_>,
26        action: &Action<'_>,
27        _args: &[&str],
28    ) -> Result<CredentialResponse, Error> {
29        let index_url = Url::parse(registry.index_url).context("parsing index url")?;
30        let sid = if let Some(name) = registry.name {
31            SourceId::for_alt_registry(&index_url, name)
32        } else {
33            SourceId::for_registry(&index_url)
34        }?;
35        let previous_token = registry_credential_config_raw(self.gctx, &sid)?.and_then(|c| c.token);
36
37        match action {
38            Action::Get(_) => {
39                let token = previous_token.ok_or_else(|| Error::NotFound)?.val;
40                Ok(CredentialResponse::Get {
41                    token,
42                    cache: CacheControl::Session,
43                    operation_independent: true,
44                })
45            }
46            Action::Login(options) => {
47                // Automatically remove `cargo login` from an inputted token to
48                // allow direct pastes from `registry.host()`/me.
49                let new_token = cargo_credential::read_token(options, registry)?
50                    .map(|line| line.replace("cargo login", "").trim().to_string());
51
52                crates_io::check_token(new_token.as_ref().expose()).map_err(Box::new)?;
53                context::save_credentials(
54                    self.gctx,
55                    Some(RegistryCredentialConfig::Token(new_token)),
56                    &sid,
57                )?;
58                let _ = self.gctx.shell().status(
59                    "Login",
60                    format!("token for `{}` saved", sid.display_registry_name()),
61                );
62                Ok(CredentialResponse::Login)
63            }
64            Action::Logout => {
65                if previous_token.is_none() {
66                    return Err(Error::NotFound);
67                }
68                let reg_name = sid.display_registry_name();
69                context::save_credentials(self.gctx, None, &sid)?;
70                let _ = self.gctx.shell().status(
71                    "Logout",
72                    format!("token for `{reg_name}` has been removed from local storage"),
73                );
74                let location = if sid.is_crates_io() {
75                    "<https://crates.io/me>".to_string()
76                } else {
77                    // The URL for the source requires network access to load the config.
78                    // That could be a fairly heavy operation to perform just to provide a
79                    // help message, so for now this just provides some generic text.
80                    // Perhaps in the future this could have an API to fetch the config if
81                    // it is cached, but avoid network access otherwise?
82                    format!("the `{reg_name}` website")
83                };
84                eprintln!(
85                    "note: This does not revoke the token on the registry server.\n    \
86                    If you need to revoke the token, visit {location} and follow the instructions there."
87                );
88                Ok(CredentialResponse::Logout)
89            }
90            _ => Err(Error::OperationNotSupported),
91        }
92    }
93}