gcp_auth/
lib.rs

1//! GCP auth provides authentication using service accounts Google Cloud Platform (GCP)
2//!
3//! GCP auth is a simple, minimal authentication library for Google Cloud Platform (GCP)
4//! providing authentication using service accounts. Once authenticated, the service
5//! account can be used to acquire bearer tokens for use in authenticating against GCP
6//! services.
7//!
8//! The library supports the following methods of retrieving tokens:
9//!
10//! 1. Reading custom service account credentials from the path pointed to by the
11//!    `GOOGLE_APPLICATION_CREDENTIALS` environment variable. Alternatively, custom service
12//!    account credentials can be read from a JSON file or string.
13//! 2. Look for credentials in `.config/gcloud/application_default_credentials.json`;
14//!    if found, use these credentials to request refresh tokens. This file can be created
15//!    by invoking `gcloud auth application-default login`.
16//! 3. Use the default service account by retrieving a token from the metadata server.
17//! 4. Retrieving a token from the `gcloud` CLI tool, if it is available on the `PATH`.
18//!
19//! For more details, see [`provider()`].
20//!
21//! A [`TokenProvider`] handles caching tokens for their lifetime; it will not make a request if
22//! an appropriate token is already cached. Therefore, the caller should not cache tokens.
23//!
24//! ## Simple usage
25//!
26//! The default way to use this library is to select the appropriate token provider using
27//! [`provider()`]. It will find the appropriate authentication method and use it to retrieve
28//! tokens.
29//!
30//! ```rust,no_run
31//! # async fn get_token() -> Result<(), gcp_auth::Error> {
32//! let provider = gcp_auth::provider().await?;
33//! let scopes = &["https://www.googleapis.com/auth/cloud-platform"];
34//! let token = provider.token(scopes).await?;
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ## Supplying service account credentials
40//!
41//! When running outside of GCP (for example, on a development machine), it can be useful to supply
42//! service account credentials. The first method checked by [`provider()`] is to
43//! read a path to a file containing JSON credentials in the `GOOGLE_APPLICATION_CREDENTIALS`
44//! environment variable. However, you may also supply a custom path to read credentials from, or
45//! a `&str` containing the credentials. In both of these cases, you should create a
46//! [`CustomServiceAccount`] directly using one of its associated functions:
47//!
48//! ```rust,no_run
49//! # use std::path::PathBuf;
50//! #
51//! # async fn get_token() -> Result<(), gcp_auth::Error> {
52//! use gcp_auth::{CustomServiceAccount, TokenProvider};
53//!
54//! // `credentials_path` variable is the path for the credentials `.json` file.
55//! let credentials_path = PathBuf::from("service-account.json");
56//! let service_account = CustomServiceAccount::from_file(credentials_path)?;
57//! let scopes = &["https://www.googleapis.com/auth/cloud-platform"];
58//! let token = service_account.token(scopes).await?;
59//! # Ok(())
60//! # }
61//! ```
62//!
63//! ## Getting tokens in multi-thread or async environments
64//!
65//! Using a `OnceCell` makes it easy to reuse the [`AuthenticationManager`] across different
66//! threads or async tasks.
67//!
68//! ```rust,no_run
69//! use std::sync::Arc;
70//! use tokio::sync::OnceCell;
71//! use gcp_auth::TokenProvider;
72//!
73//! static TOKEN_PROVIDER: OnceCell<Arc<dyn TokenProvider>> = OnceCell::const_new();
74//!
75//! async fn token_provider() -> &'static Arc<dyn TokenProvider> {
76//!     TOKEN_PROVIDER
77//!         .get_or_init(|| async {
78//!             gcp_auth::provider()
79//!                 .await
80//!                 .expect("unable to initialize token provider")
81//!         })
82//!         .await
83//! }
84//! ```
85
86#![warn(unreachable_pub)]
87
88use std::sync::Arc;
89
90use async_trait::async_trait;
91use thiserror::Error;
92use tracing::{debug, instrument, Level};
93
94mod custom_service_account;
95pub use custom_service_account::CustomServiceAccount;
96
97mod config_default_credentials;
98pub use config_default_credentials::ConfigDefaultCredentials;
99
100mod metadata_service_account;
101pub use metadata_service_account::MetadataServiceAccount;
102
103mod gcloud_authorized_user;
104pub use gcloud_authorized_user::GCloudAuthorizedUser;
105
106mod types;
107use types::HttpClient;
108pub use types::{Signer, Token};
109
110/// Finds a service account provider to get authentication tokens from
111///
112/// Tries the following approaches, in order:
113///
114/// 1. Check if the `GOOGLE_APPLICATION_CREDENTIALS` environment variable if set;
115///    if so, use a custom service account as the token source.
116/// 2. Look for credentials in `.config/gcloud/application_default_credentials.json`;
117///    if found, use these credentials to request refresh tokens.
118/// 3. Send a HTTP request to the internal metadata server to retrieve a token;
119///    if it succeeds, use the default service account as the token source.
120/// 4. Check if the `gcloud` tool is available on the `PATH`; if so, use the
121///    `gcloud auth print-access-token` command as the token source.
122#[instrument(level = Level::DEBUG)]
123pub async fn provider() -> Result<Arc<dyn TokenProvider>, Error> {
124    debug!("initializing gcp_auth");
125    if let Some(provider) = CustomServiceAccount::from_env()? {
126        return Ok(Arc::new(provider));
127    }
128
129    let client = HttpClient::new()?;
130    let default_user_error = match ConfigDefaultCredentials::with_client(&client).await {
131        Ok(provider) => {
132            debug!("using ConfigDefaultCredentials");
133            return Ok(Arc::new(provider));
134        }
135        Err(e) => e,
136    };
137
138    let default_service_error = match MetadataServiceAccount::with_client(&client).await {
139        Ok(provider) => {
140            debug!("using MetadataServiceAccount");
141            return Ok(Arc::new(provider));
142        }
143        Err(e) => e,
144    };
145
146    let gcloud_error = match GCloudAuthorizedUser::new().await {
147        Ok(provider) => {
148            debug!("using GCloudAuthorizedUser");
149            return Ok(Arc::new(provider));
150        }
151        Err(e) => e,
152    };
153
154    Err(Error::NoAuthMethod(
155        Box::new(gcloud_error),
156        Box::new(default_service_error),
157        Box::new(default_user_error),
158    ))
159}
160
161/// A trait for an authentication context that can provide tokens
162#[async_trait]
163pub trait TokenProvider: Send + Sync {
164    /// Get a valid token for the given scopes
165    ///
166    /// Tokens are cached until they expire, so this method will only fetch a fresh token once
167    /// the current token (for the given scopes) has expired.
168    async fn token(&self, scopes: &[&str]) -> Result<Arc<Token>, Error>;
169
170    /// Get the project ID for the authentication context
171    async fn project_id(&self) -> Result<Arc<str>, Error>;
172}
173
174/// Enumerates all possible errors returned by this library.
175#[derive(Error, Debug)]
176pub enum Error {
177    /// No available authentication method was discovered
178    ///
179    /// Application can authenticate against GCP using:
180    ///
181    /// - Default service account - available inside GCP platform using GCP Instance Metadata server
182    /// - GCloud authorized user - retrieved using `gcloud auth` command
183    ///
184    /// All authentication methods have been tested and none succeeded.
185    /// Service account file can be downloaded from GCP in json format.
186    #[error("no available authentication method found")]
187    NoAuthMethod(Box<Error>, Box<Error>, Box<Error>),
188
189    /// Could not connect to  server
190    #[error("{0}")]
191    Http(&'static str, #[source] hyper::Error),
192
193    #[error("{0}: {1}")]
194    Io(&'static str, #[source] std::io::Error),
195
196    #[error("{0}: {1}")]
197    Json(&'static str, #[source] serde_json::error::Error),
198
199    #[error("{0}: {1}")]
200    Other(
201        &'static str,
202        #[source] Box<dyn std::error::Error + Send + Sync>,
203    ),
204
205    #[error("{0}")]
206    Str(&'static str),
207}