google_cloud_auth/credentials.rs
1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod api_key_credentials;
16// Export API Key factory function and options
17pub use api_key_credentials::{ApiKeyOptions, create_api_key_credentials};
18
19pub mod mds;
20pub mod service_account;
21pub mod user_account;
22
23use crate::Result;
24use crate::errors::{self, CredentialsError};
25use http::header::{HeaderName, HeaderValue};
26use std::future::Future;
27use std::sync::Arc;
28
29pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
30pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
31
32/// An implementation of [crate::credentials::CredentialsTrait].
33///
34/// Represents a [Credentials] used to obtain auth [Token][crate::token::Token]s
35/// and the corresponding request headers.
36///
37/// In general, [Credentials][credentials-link] are "digital object that provide
38/// proof of identity", the archetype may be a username and password
39/// combination, but a private RSA key may be a better example.
40///
41/// Modern authentication protocols do not send the credentials to authenticate
42/// with a service. Even when sent over encrypted transports, the credentials
43/// may be accidentally exposed via logging or may be captured if there are
44/// errors in the transport encryption. Because the credentials are often
45/// long-lived, that risk of exposure is also long-lived.
46///
47/// Instead, modern authentication protocols exchange the credentials for a
48/// time-limited [Token][token-link], a digital object that shows the caller was
49/// in possession of the credentials. Because tokens are time limited, risk of
50/// misuse is also time limited. Tokens may be further restricted to only a
51/// certain subset of the RPCs in the service, or even to specific resources, or
52/// only when used from a given machine (virtual or not). Further limiting the
53/// risks associated with any leaks of these tokens.
54///
55/// This struct also abstracts token sources that are not backed by a specific
56/// digital object. The canonical example is the [Metadata Service]. This
57/// service is available in many Google Cloud environments, including
58/// [Google Compute Engine], and [Google Kubernetes Engine].
59///
60/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
61/// [token-link]: https://cloud.google.com/docs/authentication#token
62/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
63/// [Google Compute Engine]: https://cloud.google.com/products/compute
64/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
65#[derive(Clone, Debug)]
66pub struct Credentials {
67 // We use an `Arc` to hold the inner implementation.
68 //
69 // Credentials may be shared across threads (`Send + Sync`), so an `Rc`
70 // will not do.
71 //
72 // They also need to derive `Clone`, as the
73 // `gax::http_client::ReqwestClient`s which hold them derive `Clone`. So a
74 // `Box` will not do.
75 inner: Arc<dyn dynamic::CredentialsTrait>,
76}
77
78impl<T> std::convert::From<T> for Credentials
79where
80 T: crate::credentials::CredentialsTrait + Send + Sync + 'static,
81{
82 fn from(value: T) -> Self {
83 Self {
84 inner: Arc::new(value),
85 }
86 }
87}
88
89impl Credentials {
90 pub async fn token(&self) -> Result<crate::token::Token> {
91 self.inner.token().await
92 }
93
94 pub async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>> {
95 self.inner.headers().await
96 }
97
98 pub async fn universe_domain(&self) -> Option<String> {
99 self.inner.universe_domain().await
100 }
101}
102
103/// Represents a [Credentials] used to obtain auth
104/// [Token][crate::token::Token]s and the corresponding request headers.
105///
106/// In general, [Credentials][credentials-link] are "digital object that
107/// provide proof of identity", the archetype may be a username and password
108/// combination, but a private RSA key may be a better example.
109///
110/// Modern authentication protocols do not send the credentials to
111/// authenticate with a service. Even when sent over encrypted transports,
112/// the credentials may be accidentally exposed via logging or may be
113/// captured if there are errors in the transport encryption. Because the
114/// credentials are often long-lived, that risk of exposure is also
115/// long-lived.
116///
117/// Instead, modern authentication protocols exchange the credentials for a
118/// time-limited [Token][token-link], a digital object that shows the caller
119/// was in possession of the credentials. Because tokens are time limited,
120/// risk of misuse is also time limited. Tokens may be further restricted to
121/// only a certain subset of the RPCs in the service, or even to specific
122/// resources, or only when used from a given machine (virtual or not).
123/// Further limiting the risks associated with any leaks of these tokens.
124///
125/// This struct also abstracts token sources that are not backed by a
126/// specific digital object. The canonical example is the
127/// [Metadata Service]. This service is available in many Google Cloud
128/// environments, including [Google Compute Engine], and
129/// [Google Kubernetes Engine].
130///
131/// # Notes
132///
133/// Application developers who directly use the Auth SDK can use this trait,
134/// along with [crate::credentials::Credentials::from()] to mock the credentials.
135/// Application developers who use the Google Cloud Rust SDK directly should not
136/// need this functionality.
137///
138/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
139/// [token-link]: https://cloud.google.com/docs/authentication#token
140/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
141/// [Google Compute Engine]: https://cloud.google.com/products/compute
142/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
143pub trait CredentialsTrait: std::fmt::Debug {
144 /// Asynchronously retrieves a token.
145 ///
146 /// Returns a [Token][crate::token::Token] for the current credentials.
147 /// The underlying implementation refreshes the token as needed.
148 fn token(&self) -> impl Future<Output = Result<crate::token::Token>> + Send;
149
150 /// Asynchronously constructs the auth headers.
151 ///
152 /// Different auth tokens are sent via different headers. The
153 /// [Credentials] constructs the headers (and header values) that should be
154 /// sent with a request.
155 ///
156 /// The underlying implementation refreshes the token as needed.
157 fn headers(&self) -> impl Future<Output = Result<Vec<(HeaderName, HeaderValue)>>> + Send;
158
159 /// Retrieves the universe domain associated with the credentials, if any.
160 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
161}
162
163pub(crate) mod dynamic {
164 use super::Result;
165 use super::{HeaderName, HeaderValue};
166
167 /// A dyn-compatible, crate-private version of `CredentialsTrait`.
168 #[async_trait::async_trait]
169 pub trait CredentialsTrait: Send + Sync + std::fmt::Debug {
170 /// Asynchronously retrieves a token.
171 ///
172 /// Returns a [Token][crate::token::Token] for the current credentials.
173 /// The underlying implementation refreshes the token as needed.
174 async fn token(&self) -> Result<crate::token::Token>;
175
176 /// Asynchronously constructs the auth headers.
177 ///
178 /// Different auth tokens are sent via different headers. The
179 /// [Credentials] constructs the headers (and header values) that should be
180 /// sent with a request.
181 ///
182 /// The underlying implementation refreshes the token as needed.
183 async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>>;
184
185 /// Retrieves the universe domain associated with the credentials, if any.
186 async fn universe_domain(&self) -> Option<String> {
187 Some("googleapis.com".to_string())
188 }
189 }
190
191 /// The public CredentialsTrait implements the dyn-compatible CredentialsTrait.
192 #[async_trait::async_trait]
193 impl<T> CredentialsTrait for T
194 where
195 T: super::CredentialsTrait + Send + Sync,
196 {
197 async fn token(&self) -> Result<crate::token::Token> {
198 T::token(self).await
199 }
200 async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>> {
201 T::headers(self).await
202 }
203 async fn universe_domain(&self) -> Option<String> {
204 T::universe_domain(self).await
205 }
206 }
207}
208
209/// Create access token credentials.
210///
211/// Returns [Application Default Credentials (ADC)][ADC-link]. These are the
212/// most commonly used credentials, and are expected to meet the needs of most
213/// applications. They conform to [AIP-4110].
214///
215/// The access tokens returned by these credentials are to be used in the
216/// `Authorization` HTTP header.
217///
218/// Consider using these credentials when:
219///
220/// - Your application is deployed to a Google Cloud environment such as
221/// [Google Compute Engine (GCE)][gce-link],
222/// [Google Kubernetes Engine (GKE)][gke-link], or [Cloud Run]. Each of these
223/// deployment environments provides a default service account to the
224/// application, and offers mechanisms to change this default service account
225/// without any code changes to your application.
226/// - You are testing or developing the application on a workstation (physical or
227/// virtual). These credentials will use your preferences as set with
228/// [gcloud auth application-default]. These preferences can be your own GCP
229/// user credentials, or some service account.
230/// - Regardless of where your application is running, you can use the
231/// `GOOGLE_APPLICATION_CREDENTIALS` environment variable to override the
232/// defaults. This environment variable should point to a file containing a
233/// service account key file, or a JSON object describing your user
234/// credentials.
235///
236/// Example usage:
237///
238/// ```
239/// # use google_cloud_auth::credentials::create_access_token_credentials;
240/// # use google_cloud_auth::errors::CredentialsError;
241/// # tokio_test::block_on(async {
242/// let mut creds = create_access_token_credentials().await?;
243/// let token = creds.token().await?;
244/// println!("Token: {}", token.token);
245/// # Ok::<(), CredentialsError>(())
246/// # });
247/// ```
248///
249/// [ADC-link]: https://cloud.google.com/docs/authentication/application-default-credentials
250/// [AIP-4110]: https://google.aip.dev/auth/4110
251/// [Cloud Run]: https://cloud.google.com/run
252/// [gce-link]: https://cloud.google.com/products/compute
253/// [gcloud auth application-default]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default
254/// [gke-link]: https://cloud.google.com/kubernetes-engine
255pub async fn create_access_token_credentials() -> Result<Credentials> {
256 let contents = match load_adc()? {
257 AdcContents::Contents(contents) => contents,
258 AdcContents::FallbackToMds => return Ok(mds::new()),
259 };
260 let js: serde_json::Value = serde_json::from_str(&contents).map_err(errors::non_retryable)?;
261 let cred_type = js
262 .get("type")
263 .ok_or_else(|| errors::non_retryable_from_str("Failed to parse Application Default Credentials (ADC). No `type` field found."))?
264 .as_str()
265 .ok_or_else(|| errors::non_retryable_from_str("Failed to parse Application Default Credentials (ADC). `type` field is not a string.")
266 )?;
267 match cred_type {
268 "authorized_user" => user_account::creds_from(js),
269 "service_account" => service_account::creds_from(js),
270 _ => Err(errors::non_retryable_from_str(format!(
271 "Unimplemented credentials type: {cred_type}"
272 ))),
273 }
274}
275
276#[derive(Debug, PartialEq)]
277enum AdcPath {
278 FromEnv(String),
279 WellKnown(String),
280}
281
282#[derive(Debug, PartialEq)]
283enum AdcContents {
284 Contents(String),
285 FallbackToMds,
286}
287
288fn path_not_found(path: String) -> CredentialsError {
289 errors::non_retryable_from_str(format!(
290 "Failed to load Application Default Credentials (ADC) from {path}. Check that the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid file."
291 ))
292}
293
294fn load_adc() -> Result<AdcContents> {
295 match adc_path() {
296 None => Ok(AdcContents::FallbackToMds),
297 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
298 Ok(contents) => Ok(AdcContents::Contents(contents)),
299 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
300 Err(e) => Err(errors::non_retryable(e)),
301 },
302 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
303 Ok(contents) => Ok(AdcContents::Contents(contents)),
304 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
305 Err(e) => Err(errors::non_retryable(e)),
306 },
307 }
308}
309
310/// The path to Application Default Credentials (ADC), as specified in [AIP-4110].
311///
312/// [AIP-4110]: https://google.aip.dev/auth/4110
313fn adc_path() -> Option<AdcPath> {
314 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
315 return Some(AdcPath::FromEnv(path));
316 }
317 Some(AdcPath::WellKnown(adc_well_known_path()?))
318}
319
320/// The well-known path to ADC on Windows, as specified in [AIP-4113].
321///
322/// [AIP-4113]: https://google.aip.dev/auth/4113
323#[cfg(target_os = "windows")]
324fn adc_well_known_path() -> Option<String> {
325 std::env::var("APPDATA")
326 .ok()
327 .map(|root| root + "/gcloud/application_default_credentials.json")
328}
329
330/// The well-known path to ADC on Linux and Mac, as specified in [AIP-4113].
331///
332/// [AIP-4113]: https://google.aip.dev/auth/4113
333#[cfg(not(target_os = "windows"))]
334fn adc_well_known_path() -> Option<String> {
335 std::env::var("HOME")
336 .ok()
337 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
338}
339
340/// A module providing invalid credentials where authentication does not matter.
341///
342/// These credentials are a convenient way to avoid errors from loading
343/// Application Default Credentials in tests.
344///
345/// This module is mainly relevant to other `google-cloud-*` crates, but some
346/// external developers (i.e. consumers, not developers of `google-cloud-rust`)
347/// may find it useful.
348pub mod testing {
349 use crate::Result;
350 use crate::credentials::Credentials;
351 use crate::credentials::dynamic::CredentialsTrait;
352 use crate::token::Token;
353 use http::header::{HeaderName, HeaderValue};
354 use std::sync::Arc;
355
356 /// A simple credentials implementation to use in tests where authentication does not matter.
357 ///
358 /// Always returns a "Bearer" token, with "test-only-token" as the value.
359 pub fn test_credentials() -> Credentials {
360 Credentials {
361 inner: Arc::from(TestCredentials {}),
362 }
363 }
364
365 #[derive(Debug)]
366 struct TestCredentials;
367
368 #[async_trait::async_trait]
369 impl CredentialsTrait for TestCredentials {
370 async fn token(&self) -> Result<Token> {
371 Ok(Token {
372 token: "test-only-token".to_string(),
373 token_type: "Bearer".to_string(),
374 expires_at: None,
375 metadata: None,
376 })
377 }
378
379 async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>> {
380 Ok(Vec::new())
381 }
382
383 async fn universe_domain(&self) -> Option<String> {
384 None
385 }
386 }
387
388 /// A simple credentials implementation to use in tests.
389 ///
390 /// Always return an error in `token()` and `headers()`.
391 pub fn error_credentials(retryable: bool) -> Credentials {
392 Credentials {
393 inner: Arc::from(ErrorCredentials(retryable)),
394 }
395 }
396
397 #[derive(Debug, Default)]
398 struct ErrorCredentials(bool);
399
400 #[async_trait::async_trait]
401 impl CredentialsTrait for ErrorCredentials {
402 async fn token(&self) -> Result<Token> {
403 Err(super::CredentialsError::from_str(self.0, "test-only"))
404 }
405
406 async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>> {
407 Err(super::CredentialsError::from_str(self.0, "test-only"))
408 }
409
410 async fn universe_domain(&self) -> Option<String> {
411 None
412 }
413 }
414}
415
416#[cfg(test)]
417mod test {
418 use super::*;
419 use scoped_env::ScopedEnv;
420 use std::error::Error;
421 use test_case::test_case;
422
423 // Convenience struct for verifying (HeaderName, HeaderValue) pairs.
424 #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
425 pub struct HV {
426 pub header: String,
427 pub value: String,
428 pub is_sensitive: bool,
429 }
430
431 impl HV {
432 pub fn from(headers: Vec<(HeaderName, HeaderValue)>) -> Vec<HV> {
433 let mut hvs: Vec<HV> = headers
434 .into_iter()
435 .map(|(h, v)| HV {
436 header: h.to_string(),
437 value: v.to_str().unwrap().to_string(),
438 is_sensitive: v.is_sensitive(),
439 })
440 .collect();
441
442 // We want to verify the contents of the headers. We do not care
443 // what order they are in.
444 hvs.sort();
445 hvs
446 }
447 }
448
449 #[cfg(target_os = "windows")]
450 #[test]
451 #[serial_test::serial]
452 fn adc_well_known_path_windows() {
453 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
454 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
455 assert_eq!(
456 adc_well_known_path(),
457 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
458 );
459 assert_eq!(
460 adc_path(),
461 Some(AdcPath::WellKnown(
462 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
463 ))
464 );
465 }
466
467 #[cfg(target_os = "windows")]
468 #[test]
469 #[serial_test::serial]
470 fn adc_well_known_path_windows_no_appdata() {
471 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
472 let _appdata = ScopedEnv::remove("APPDATA");
473 assert_eq!(adc_well_known_path(), None);
474 assert_eq!(adc_path(), None);
475 }
476
477 #[cfg(not(target_os = "windows"))]
478 #[test]
479 #[serial_test::serial]
480 fn adc_well_known_path_posix() {
481 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
482 let _home = ScopedEnv::set("HOME", "/home/foo");
483 assert_eq!(
484 adc_well_known_path(),
485 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
486 );
487 assert_eq!(
488 adc_path(),
489 Some(AdcPath::WellKnown(
490 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
491 ))
492 );
493 }
494
495 #[cfg(not(target_os = "windows"))]
496 #[test]
497 #[serial_test::serial]
498 fn adc_well_known_path_posix_no_home() {
499 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
500 let _appdata = ScopedEnv::remove("HOME");
501 assert_eq!(adc_well_known_path(), None);
502 assert_eq!(adc_path(), None);
503 }
504
505 #[test]
506 #[serial_test::serial]
507 fn adc_path_from_env() {
508 let _creds = ScopedEnv::set(
509 "GOOGLE_APPLICATION_CREDENTIALS",
510 "/usr/bar/application_default_credentials.json",
511 );
512 assert_eq!(
513 adc_path(),
514 Some(AdcPath::FromEnv(
515 "/usr/bar/application_default_credentials.json".to_string()
516 ))
517 );
518 }
519
520 #[test]
521 #[serial_test::serial]
522 fn load_adc_no_well_known_path_fallback_to_mds() {
523 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
524 let _e2 = ScopedEnv::remove("HOME"); // For posix
525 let _e3 = ScopedEnv::remove("APPDATA"); // For windows
526 assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
527 }
528
529 #[test]
530 #[serial_test::serial]
531 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
532 // Create a new temp directory. There is not an ADC file in here.
533 let dir = tempfile::TempDir::new().unwrap();
534 let path = dir.path().to_str().unwrap();
535 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
536 let _e2 = ScopedEnv::set("HOME", path); // For posix
537 let _e3 = ScopedEnv::set("APPDATA", path); // For windows
538 assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
539 }
540
541 #[test]
542 #[serial_test::serial]
543 fn load_adc_no_file_at_env_is_error() {
544 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
545 let err = load_adc().err().unwrap();
546 let msg = err.source().unwrap().to_string();
547 assert!(msg.contains("Failed to load Application Default Credentials"));
548 assert!(msg.contains("file-does-not-exist.json"));
549 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"));
550 }
551
552 #[test]
553 #[serial_test::serial]
554 fn load_adc_success() {
555 let file = tempfile::NamedTempFile::new().unwrap();
556 let path = file.into_temp_path();
557 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
558 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
559
560 assert_eq!(
561 load_adc().unwrap(),
562 AdcContents::Contents("contents".to_string())
563 );
564 }
565
566 #[test_case(true; "retryable")]
567 #[test_case(false; "non-retryable")]
568 #[tokio::test]
569 async fn error_credentials(retryable: bool) {
570 let credentials = super::testing::error_credentials(retryable);
571 assert!(
572 credentials.universe_domain().await.is_none(),
573 "{credentials:?}"
574 );
575 let err = credentials.token().await.err().unwrap();
576 assert_eq!(err.is_retryable(), retryable, "{err:?}");
577 let err = credentials.headers().await.err().unwrap();
578 assert_eq!(err.is_retryable(), retryable, "{err:?}");
579 }
580}