tame_oauth/
gcp.rs

1//! Provides functionality for
2//! [Google oauth](https://developers.google.com/identity/protocols/oauth2)
3
4use crate::token_cache::CachedTokenProvider;
5use crate::{error::Error, jwt};
6
7pub mod end_user;
8pub mod metadata_server;
9pub mod service_account;
10
11use end_user as eu;
12use metadata_server as ms;
13use service_account as sa;
14
15pub use crate::id_token::{
16    AccessTokenResponse, IdToken, IdTokenOrRequest, IdTokenProvider, IdTokenRequest,
17    IdTokenResponse,
18};
19pub use crate::token::{Token, TokenOrRequest, TokenProvider};
20pub use {
21    end_user::{EndUserCredentials, EndUserCredentialsInfo},
22    metadata_server::MetadataServerProvider,
23    service_account::{ServiceAccountInfo, ServiceAccountProvider},
24};
25
26/// Both the [`ServiceAccountProvider`] and [`MetadataServerProvider`] get back
27/// JSON responses with this schema from their endpoints.
28#[derive(serde::Deserialize, Debug)]
29struct TokenResponse {
30    /// The actual token
31    access_token: String,
32    /// The token type, pretty much always Header
33    token_type: String,
34    /// The time until the token expires and a new one needs to be requested
35    expires_in: i64,
36}
37
38pub type TokenProviderWrapper = CachedTokenProvider<TokenProviderWrapperInner>;
39impl TokenProviderWrapper {
40    /// Get a `TokenProvider` following the "Google Default Credentials"
41    /// flow, in order:
42    ///
43    /// * If the `GOOGLE_APPLICATION_CREDENTIALS` environment variable is
44    ///   set, use that as a path to a [`ServiceAccountInfo`](sa::ServiceAccountInfo).
45    ///
46    /// * Check for a gcloud's
47    /// [Application Default Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default)
48    /// for [`EndUserCredentials`](eu::EndUserCredentials)
49    ///
50    /// * If we're running on GCP, use the local metadata server.
51    ///
52    /// * Otherwise, return None.
53    ///
54    /// If it appears that a method is being used, but is actually invalid,
55    /// eg `GOOGLE_APPLICATION_CREDENTIALS` is set but the file doesn't exist or
56    /// contains invalid JSON, an error is returned with the details
57    pub fn get_default_provider() -> Result<Option<Self>, Error> {
58        TokenProviderWrapperInner::get_default_provider()
59            .map(|provider| provider.map(CachedTokenProvider::wrap))
60    }
61
62    /// Gets the kind of token provider
63    pub fn kind(&self) -> &'static str {
64        self.inner().kind()
65    }
66
67    pub fn is_service_account_provider(&self) -> bool {
68        self.inner().is_service_account_provider()
69    }
70    pub fn is_metadata_server_provider(&self) -> bool {
71        self.inner().is_metadata_server_provider()
72    }
73    pub fn is_end_user_credentials_provider(&self) -> bool {
74        self.inner().is_end_user_credentials_provider()
75    }
76}
77
78/// Wrapper around the different providers that are supported. Implements both `TokenProvider` and `IdTokenProvider`.
79/// Should not be used directly as it is not cached. Use `TokenProviderWrapper` instead.
80#[derive(Debug)]
81pub enum TokenProviderWrapperInner {
82    EndUser(eu::EndUserCredentialsInner),
83    Metadata(ms::MetadataServerProviderInner),
84    ServiceAccount(sa::ServiceAccountProviderInner),
85}
86
87impl TokenProviderWrapperInner {
88    /// Get a `TokenProvider` following the "Google Default Credentials" flow.
89    /// Returns a uncached token provider, use `TokenProviderWrapper::get_default_provider`
90    /// instead.
91    pub fn get_default_provider() -> Result<Option<Self>, Error> {
92        use std::{fs::read_to_string, path::PathBuf};
93
94        // If the environment variable is present, try to open it as a
95        // Service Account.
96        if let Some(cred_path) = std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS") {
97            let key_data = match read_to_string(&cred_path) {
98                Ok(kd) => kd,
99                Err(e) => {
100                    return Err(Error::InvalidCredentials {
101                        file: cred_path.into(),
102                        error: Box::new(Error::Io(e)),
103                    });
104                }
105            };
106
107            let sa_info = match sa::ServiceAccountInfo::deserialize(key_data) {
108                Ok(si) => si,
109                Err(e) => {
110                    return Err(Error::InvalidCredentials {
111                        file: cred_path.into(),
112                        error: Box::new(e),
113                    });
114                }
115            };
116
117            return Ok(Some(TokenProviderWrapperInner::ServiceAccount(
118                sa::ServiceAccountProviderInner::new(sa_info).map_err(|e| {
119                    Error::InvalidCredentials {
120                        file: cred_path.into(),
121                        error: Box::new(e),
122                    }
123                })?,
124            )));
125        }
126
127        /// Get the path to the gcloud `application_default_credentials.json`
128        /// file. This function respects the `CLOUDSDK_CONFIG` environment
129        /// variable. If unset, it looks in the platform-specific gcloud
130        /// configuration directories
131        fn gcloud_config_file() -> Option<PathBuf> {
132            let cred_file = "application_default_credentials.json";
133
134            // If the user has set CLOUDSDK_CONFIG, that overrides the default directory.
135            if let Some(override_dir) = std::env::var_os("CLOUDSDK_CONFIG") {
136                let mut pb = PathBuf::from(override_dir);
137                pb.push(cred_file);
138                return Some(pb);
139            }
140
141            // Otherwise, use the default for the platform.
142            // * Windows - %APPDATA%/gcloud/<file>
143            // * Unix - $HOME/.config/gcloud/<file>
144            if cfg!(windows) {
145                std::env::var_os("APPDATA").map(PathBuf::from)
146            } else {
147                std::env::var_os("HOME").map(|pb| {
148                    let mut pb = PathBuf::from(pb);
149                    pb.push(".config");
150                    pb
151                })
152            }
153            .map(|mut bd| {
154                bd.push("gcloud");
155                bd.push(cred_file);
156                bd
157            })
158        }
159
160        if let Some(gcloud_file) = gcloud_config_file() {
161            match read_to_string(&gcloud_file) {
162                Ok(json_data) => {
163                    let end_user_credentials = eu::EndUserCredentialsInfo::deserialize(json_data)
164                        .map_err(|e| Error::InvalidCredentials {
165                        file: gcloud_file,
166                        error: Box::new(e),
167                    })?;
168
169                    return Ok(Some(TokenProviderWrapperInner::EndUser(
170                        eu::EndUserCredentialsInner::new(end_user_credentials),
171                    )));
172                }
173                // Skip not found errors, and fall back to the metadata server check
174                Err(nf) if nf.kind() == std::io::ErrorKind::NotFound => {}
175                Err(err) => {
176                    return Err(Error::InvalidCredentials {
177                        file: gcloud_file,
178                        error: Box::new(Error::Io(err)),
179                    });
180                }
181            }
182        }
183
184        // Finally, if we are on GCP, use the metadata server. If we're not on
185        // GCP, this will just fail to read the file.
186        if let Ok(full_name) = read_to_string("/sys/class/dmi/id/product_name") {
187            // The product name can annoyingly include a newline...
188            let trimmed = full_name.trim();
189            match trimmed {
190                // This matches the Golang client. If new products
191                // add additional values, this will need to be updated.
192                "Google" | "Google Compute Engine" => {
193                    return Ok(Some(TokenProviderWrapperInner::Metadata(
194                        ms::MetadataServerProviderInner::new(None),
195                    )));
196                }
197                _ => {}
198            }
199        }
200
201        // None of our checks worked. Give up.
202        Ok(None)
203    }
204
205    /// Gets the kind of token provider
206    pub fn kind(&self) -> &'static str {
207        match self {
208            Self::EndUser(_) => "End User",
209            Self::Metadata(_) => "Metadata Server",
210            Self::ServiceAccount(_) => "Service Account",
211        }
212    }
213
214    pub fn is_service_account_provider(&self) -> bool {
215        matches!(self, TokenProviderWrapperInner::ServiceAccount(_))
216    }
217    pub fn is_metadata_server_provider(&self) -> bool {
218        matches!(self, TokenProviderWrapperInner::Metadata(_))
219    }
220    pub fn is_end_user_credentials_provider(&self) -> bool {
221        matches!(self, TokenProviderWrapperInner::EndUser(_))
222    }
223}
224
225impl TokenProvider for TokenProviderWrapperInner {
226    fn get_token_with_subject<'a, S, I, T>(
227        &self,
228        subject: Option<T>,
229        scopes: I,
230    ) -> Result<TokenOrRequest, Error>
231    where
232        S: AsRef<str> + 'a,
233        I: IntoIterator<Item = &'a S> + Clone,
234        T: Into<String>,
235    {
236        match self {
237            Self::EndUser(token_provider) => token_provider.get_token_with_subject(subject, scopes),
238            Self::Metadata(token_provider) => {
239                token_provider.get_token_with_subject(subject, scopes)
240            }
241            Self::ServiceAccount(token_provider) => {
242                token_provider.get_token_with_subject(subject, scopes)
243            }
244        }
245    }
246
247    fn parse_token_response<S>(
248        &self,
249        hash: u64,
250        response: http::Response<S>,
251    ) -> Result<Token, Error>
252    where
253        S: AsRef<[u8]>,
254    {
255        match self {
256            Self::EndUser(token_provider) => token_provider.parse_token_response(hash, response),
257            Self::Metadata(token_provider) => token_provider.parse_token_response(hash, response),
258            Self::ServiceAccount(token_provider) => {
259                token_provider.parse_token_response(hash, response)
260            }
261        }
262    }
263}
264
265impl IdTokenProvider for TokenProviderWrapperInner {
266    fn get_id_token(&self, audience: &str) -> Result<IdTokenOrRequest, Error> {
267        match self {
268            Self::EndUser(token_provider) => token_provider.get_id_token(audience),
269            Self::Metadata(token_provider) => token_provider.get_id_token(audience),
270            Self::ServiceAccount(token_provider) => token_provider.get_id_token(audience),
271        }
272    }
273
274    fn get_id_token_with_access_token<S>(
275        &self,
276        audience: &str,
277        response: AccessTokenResponse<S>,
278    ) -> Result<IdTokenRequest, Error>
279    where
280        S: AsRef<[u8]>,
281    {
282        match self {
283            Self::EndUser(token_provider) => {
284                token_provider.get_id_token_with_access_token(audience, response)
285            }
286            Self::Metadata(token_provider) => {
287                token_provider.get_id_token_with_access_token(audience, response)
288            }
289            Self::ServiceAccount(token_provider) => {
290                token_provider.get_id_token_with_access_token(audience, response)
291            }
292        }
293    }
294
295    fn parse_id_token_response<S>(
296        &self,
297        hash: u64,
298        response: http::Response<S>,
299    ) -> Result<IdToken, Error>
300    where
301        S: AsRef<[u8]>,
302    {
303        match self {
304            Self::EndUser(token_provider) => token_provider.parse_id_token_response(hash, response),
305            Self::Metadata(token_provider) => {
306                token_provider.parse_id_token_response(hash, response)
307            }
308            Self::ServiceAccount(token_provider) => {
309                token_provider.parse_id_token_response(hash, response)
310            }
311        }
312    }
313}
314
315impl From<TokenResponse> for Token {
316    fn from(tr: TokenResponse) -> Self {
317        Self {
318            access_token: tr.access_token,
319            token_type: tr.token_type,
320            refresh_token: String::new(),
321            expires_in: Some(tr.expires_in),
322            expires_in_timestamp: std::time::SystemTime::now()
323                .checked_add(std::time::Duration::from_secs(tr.expires_in as u64)),
324        }
325    }
326}