Skip to main content

gcloud_sdk/token_source/
mod.rs

1use std::convert::TryFrom;
2use std::fmt::Debug;
3use std::ops::Add;
4use std::path::PathBuf;
5
6use async_trait::async_trait;
7use jiff::{SignedDuration, Timestamp};
8use secret_vault_value::SecretValue;
9
10pub mod auth_token_generator;
11pub mod credentials;
12pub mod id_token;
13pub mod metadata;
14
15pub use credentials::{from_env_var, from_well_known_file};
16use metadata::from_metadata;
17
18pub use credentials::{from_file, from_json};
19use tracing::*;
20
21mod ext_creds_source;
22mod gce;
23
24pub type BoxSource = Box<dyn Source + Send + Sync + 'static>;
25
26#[async_trait]
27pub trait Source {
28    async fn token(&self) -> crate::error::Result<Token>;
29}
30
31pub async fn create_source(
32    token_source_type: TokenSourceType,
33    token_scopes: Vec<String>,
34) -> crate::error::Result<BoxSource> {
35    match token_source_type {
36        TokenSourceType::Default => Ok(find_default(&token_scopes).await?),
37        TokenSourceType::Json(json) => Ok(from_json(json.as_bytes(), &token_scopes)?.into()),
38        TokenSourceType::File(path) => Ok(from_file(path, &token_scopes)?.into()),
39        TokenSourceType::MetadataServer => {
40            if let Some(src) = from_metadata(&token_scopes, "default".to_string()).await? {
41                Ok(src.into())
42            } else {
43                Err(crate::error::ErrorKind::TokenSource.into())
44            }
45        }
46        TokenSourceType::MetadataServerWithAccount(account) => {
47            if let Some(src) = from_metadata(&token_scopes, account).await? {
48                Ok(src.into())
49            } else {
50                Err(crate::error::ErrorKind::TokenSource.into())
51            }
52        }
53        TokenSourceType::ExternalSource(token_source) => Ok(token_source),
54    }
55}
56
57// Looks for credentials in the following places, preferring the first location found:
58// - A JSON file whose path is specified by the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
59// - A JSON file in a location known to the gcloud command-line tool.
60// - On Google Compute Engine, it fetches credentials from the metadata server.
61pub async fn find_default(token_scopes: &[String]) -> crate::error::Result<BoxSource> {
62    Ok(find_default_credentials(token_scopes).await?.into())
63}
64
65/// The credentials [`find_default`] settles on, before they are boxed into a token source.
66enum DefaultCredentials {
67    File(credentials::Credentials),
68    MetadataServer(metadata::Metadata),
69}
70
71impl From<DefaultCredentials> for BoxSource {
72    fn from(v: DefaultCredentials) -> Self {
73        match v {
74            DefaultCredentials::File(credentials) => credentials.into(),
75            DefaultCredentials::MetadataServer(metadata) => metadata.into(),
76        }
77    }
78}
79
80async fn find_default_credentials(
81    token_scopes: &[String],
82) -> crate::error::Result<DefaultCredentials> {
83    debug!("Finding default token for scopes: {:?}", token_scopes);
84
85    if let Some(src) = from_env_var(token_scopes)? {
86        debug!("Creating token based on environment variable: GOOGLE_APPLICATION_CREDENTIALS");
87        return Ok(DefaultCredentials::File(src));
88    }
89    if let Some(src) = from_well_known_file(token_scopes)? {
90        debug!("Creating token based on standard config files such as application_default_credentials.json");
91        return Ok(DefaultCredentials::File(src));
92    }
93    if let Some(src) = from_metadata(token_scopes, "default".to_string()).await? {
94        debug!("Creating token based on metadata server");
95        return Ok(DefaultCredentials::MetadataServer(src));
96    }
97    warn!("None of the possible sources detected for Google OAuth token");
98    Err(crate::error::ErrorKind::TokenSource.into())
99}
100
101#[derive(Debug, Clone)]
102pub struct Token {
103    pub token_type: String,
104    pub token: SecretValue,
105    pub expiry: Timestamp,
106}
107
108impl Token {
109    pub fn new(token_type: String, token: SecretValue, expiry: Timestamp) -> Self {
110        Self {
111            token_type,
112            token,
113            expiry,
114        }
115    }
116    pub fn header_value(&self) -> String {
117        format!("{} {}", self.token_type, self.token.as_sensitive_str())
118    }
119
120    /// The `authorization` header value for this token, marked sensitive so that it is
121    /// left out of logs.
122    pub(crate) fn authorization(&self) -> crate::error::Result<hyper::header::HeaderValue> {
123        let mut authorization = hyper::header::HeaderValue::from_str(&self.header_value())?;
124        authorization.set_sensitive(true);
125        Ok(authorization)
126    }
127
128    pub async fn generate_for_scopes(
129        token_source_type: TokenSourceType,
130        token_scopes: Vec<String>,
131    ) -> crate::error::Result<Token> {
132        let token_source: BoxSource = create_source(token_source_type, token_scopes).await?;
133        token_source.token().await
134    }
135}
136
137impl TryFrom<TokenResponse> for Token {
138    type Error = crate::error::Error;
139
140    fn try_from(v: TokenResponse) -> Result<Self, Self::Error> {
141        if v.token_type.is_empty()
142            || v.access_token.as_sensitive_bytes().is_empty()
143            || v.expires_in == 0
144        {
145            Err(crate::error::ErrorKind::TokenData.into())
146        } else {
147            Ok(Token {
148                token_type: v.token_type,
149                token: v.access_token,
150                expiry: Timestamp::now()
151                    .add(SignedDuration::from_secs(v.expires_in.try_into().unwrap())),
152            })
153        }
154    }
155}
156
157#[derive(Debug, serde::Deserialize)]
158struct TokenResponse {
159    token_type: String,
160    access_token: SecretValue,
161    expires_in: u64,
162}
163
164impl TryFrom<&str> for TokenResponse {
165    type Error = crate::error::Error;
166
167    fn try_from(v: &str) -> Result<Self, Self::Error> {
168        let resp = serde_json::from_str(v).map_err(crate::error::ErrorKind::TokenJson)?;
169        Ok(resp)
170    }
171}
172
173#[derive(Debug, Clone)]
174pub struct ExternalJwtFunctionSource<F, FN>
175where
176    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
177    FN: Fn() -> F + Send + Sync,
178{
179    token_fn: FN,
180}
181
182impl<F, FN> ExternalJwtFunctionSource<F, FN>
183where
184    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync + 'static,
185    FN: Fn() -> F + Send + Sync,
186{
187    pub fn new(token_fn: FN) -> Self {
188        Self { token_fn }
189    }
190}
191
192#[async_trait]
193impl<F, FN> Source for ExternalJwtFunctionSource<F, FN>
194where
195    F: std::future::Future<Output = crate::error::Result<Token>> + Send + Sync,
196    FN: Fn() -> F + Send + Sync,
197{
198    async fn token(&self) -> crate::error::Result<Token> {
199        (self.token_fn)().await
200    }
201}
202
203pub enum TokenSourceType {
204    Default,
205    Json(String),
206    File(PathBuf),
207    MetadataServer,
208    MetadataServerWithAccount(String),
209    ExternalSource(BoxSource),
210}
211
212impl Debug for TokenSourceType {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        match self {
215            TokenSourceType::Default => write!(f, "Default"),
216            TokenSourceType::Json(_) => write!(f, "Json"),
217            TokenSourceType::File(_) => write!(f, "File"),
218            TokenSourceType::MetadataServer => write!(f, "MetadataServer"),
219            TokenSourceType::MetadataServerWithAccount(_) => write!(f, "MetadataServerWithAccount"),
220            TokenSourceType::ExternalSource(_) => write!(f, "ExternalSource"),
221        }
222    }
223}
224
225#[cfg(test)]
226mod test {
227    use super::*;
228
229    macro_rules! test_token_try_from {
230        () => {};
231        ($name:ident, $in:expr, $ok:expr; $($tt:tt)*) => {
232            #[test]
233            fn $name() {
234                assert_eq!(Token::try_from($in).is_ok(), $ok)
235            }
236            test_token_try_from!($($tt)*);
237        };
238    }
239
240    test_token_try_from!(
241        test_token_try_from_token_type,
242        TokenResponse {
243            token_type: String::new(),
244            access_token: "secret".into(),
245            expires_in: 1,
246        },
247        false;
248
249        test_token_try_from_access_token,
250        TokenResponse {
251            token_type: "type".into(),
252            access_token: "".into(),
253            expires_in: 1,
254        },
255        false;
256
257        test_token_try_from_expires_in,
258        TokenResponse {
259            token_type: "type".into(),
260            access_token: "secret".into(),
261            expires_in: 0,
262        },
263        false;
264
265        test_token_try_from_ok,
266        TokenResponse {
267            token_type: "type".into(),
268            access_token: "secret".into(),
269            expires_in: 1,
270        },
271        true;
272    );
273}