gcp_auth/
config_default_credentials.rs

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