Skip to main content

gcp_auth/
config_default_credentials.rs

1#[cfg(target_family = "unix")]
2use std::env;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use bytes::Bytes;
8use http_body_util::Full;
9use hyper::header::CONTENT_TYPE;
10use hyper::{Method, Request};
11use serde::Serialize;
12use tokio::sync::RwLock;
13use tracing::{debug, instrument, Level};
14
15use crate::types::{AuthorizedUserRefreshToken, HttpClient, Token};
16use crate::{Error, TokenProvider};
17
18/// A token provider that uses the default user credentials
19///
20/// Reads credentials from `.config/gcloud/application_default_credentials.json` on Linux and MacOS
21/// or from `%APPDATA%/gcloud/application_default_credentials.json` on Windows.
22/// See [GCloud Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials#personal)
23/// for details.
24#[derive(Debug)]
25pub struct ConfigDefaultCredentials {
26    client: HttpClient,
27    token: RwLock<Arc<Token>>,
28    credentials: AuthorizedUserRefreshToken,
29}
30
31impl ConfigDefaultCredentials {
32    /// Check for user credentials in the default location and try to deserialize them
33    pub async fn new() -> Result<Self, Error> {
34        let client = HttpClient::new()?;
35        Self::with_client(&client).await
36    }
37
38    pub(crate) async fn with_client(client: &HttpClient) -> Result<Self, Error> {
39        debug!("try to load credentials from configuration");
40        let mut config_path = config_dir()?;
41        config_path.push(USER_CREDENTIALS_PATH);
42        debug!(config = config_path.to_str(), "reading configuration file");
43
44        let credentials = AuthorizedUserRefreshToken::from_file(&config_path)?;
45        debug!(project = ?credentials.quota_project_id, client = credentials.client_id, "found user credentials");
46
47        Ok(Self {
48            client: client.clone(),
49            token: RwLock::new(Self::fetch_token(&credentials, client).await?),
50            credentials,
51        })
52    }
53
54    #[instrument(level = Level::DEBUG, skip(cred, client))]
55    async fn fetch_token(
56        cred: &AuthorizedUserRefreshToken,
57        client: &HttpClient,
58    ) -> Result<Arc<Token>, Error> {
59        client
60            .token(
61                &|| {
62                    Request::builder()
63                        .method(Method::POST)
64                        .uri(DEFAULT_TOKEN_GCP_URI)
65                        .header(CONTENT_TYPE, "application/json")
66                        .body(Full::from(Bytes::from(
67                            serde_json::to_vec(&RefreshRequest {
68                                client_id: &cred.client_id,
69                                client_secret: &cred.client_secret,
70                                grant_type: "refresh_token",
71                                refresh_token: &cred.refresh_token,
72                            })
73                            .unwrap(),
74                        )))
75                        .unwrap()
76                },
77                "ConfigDefaultCredentials",
78            )
79            .await
80    }
81}
82
83#[async_trait]
84impl TokenProvider for ConfigDefaultCredentials {
85    async fn token(&self, _scopes: &[&str]) -> Result<Arc<Token>, Error> {
86        let token = self.token.read().await.clone();
87        if !token.has_expired() {
88            return Ok(token);
89        }
90
91        let mut locked = self.token.write().await;
92        let token = Self::fetch_token(&self.credentials, &self.client).await?;
93        *locked = token.clone();
94        Ok(token)
95    }
96
97    async fn project_id(&self) -> Result<Arc<str>, Error> {
98        self.credentials
99            .quota_project_id
100            .clone()
101            .ok_or(Error::Str("no project ID in user credentials"))
102    }
103}
104
105#[derive(Serialize, Debug)]
106struct RefreshRequest<'a> {
107    client_id: &'a str,
108    client_secret: &'a str,
109    grant_type: &'a str,
110    refresh_token: &'a str,
111}
112
113#[cfg(target_family = "unix")]
114fn config_dir() -> Result<PathBuf, Error> {
115    let mut home = env::home_dir().ok_or(Error::Str("home directory not found"))?;
116    home.push(CONFIG_DIR);
117    Ok(home)
118}
119
120#[cfg(target_family = "windows")]
121fn config_dir() -> Result<PathBuf, Error> {
122    let app_data = std::env::var(ENV_APPDATA)
123        .map_err(|_| Error::Str("APPDATA environment variable not found"))?;
124    let config_path = PathBuf::from(app_data);
125    match config_path.exists() {
126        true => Ok(config_path),
127        false => Err(Error::Str("APPDATA directory not found")),
128    }
129}
130
131const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
132const USER_CREDENTIALS_PATH: &str = "gcloud/application_default_credentials.json";
133
134#[cfg(target_family = "unix")]
135const CONFIG_DIR: &str = ".config";
136
137#[cfg(target_family = "windows")]
138const ENV_APPDATA: &str = "APPDATA";