Skip to main content

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//! # #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
50//! # use std::path::PathBuf;
51//! #
52//! # #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
53//! # async fn get_token() -> Result<(), gcp_auth::Error> {
54//! use gcp_auth::{CustomServiceAccount, TokenProvider};
55//!
56//! // `credentials_path` variable is the path for the credentials `.json` file.
57//! let credentials_path = PathBuf::from("service-account.json");
58//! let service_account = CustomServiceAccount::from_file(credentials_path)?;
59//! let scopes = &["https://www.googleapis.com/auth/cloud-platform"];
60//! let token = service_account.token(scopes).await?;
61//! # Ok(())
62//! # }
63//! ```
64//!
65//! ## Getting tokens in multi-thread or async environments
66//!
67//! Using a `OnceCell` makes it easy to reuse the [`AuthenticationManager`] across different
68//! threads or async tasks.
69//!
70//! ```rust,no_run
71//! use std::sync::Arc;
72//! use tokio::sync::OnceCell;
73//! use gcp_auth::TokenProvider;
74//!
75//! static TOKEN_PROVIDER: OnceCell<Arc<dyn TokenProvider>> = OnceCell::const_new();
76//!
77//! async fn token_provider() -> &'static Arc<dyn TokenProvider> {
78//!     TOKEN_PROVIDER
79//!         .get_or_init(|| async {
80//!             gcp_auth::provider()
81//!                 .await
82//!                 .expect("unable to initialize token provider")
83//!         })
84//!         .await
85//! }
86//! ```
87
88#![warn(unreachable_pub)]
89
90use std::sync::Arc;
91
92use async_trait::async_trait;
93use thiserror::Error;
94use tracing::{debug, instrument, Level};
95
96#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
97mod custom_service_account;
98#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
99pub use custom_service_account::CustomServiceAccount;
100
101mod config_default_credentials;
102pub use config_default_credentials::ConfigDefaultCredentials;
103
104mod metadata_service_account;
105pub use metadata_service_account::MetadataServiceAccount;
106
107mod gcloud_authorized_user;
108pub use gcloud_authorized_user::GCloudAuthorizedUser;
109
110mod types;
111use types::HttpClient;
112#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
113pub use types::Signer;
114pub use types::Token;
115
116/// Finds a service account provider to get authentication tokens from
117///
118/// Tries the following approaches, in order:
119///
120/// 1. Check if the `GOOGLE_APPLICATION_CREDENTIALS` environment variable if set;
121///    if so, use a custom service account as the token source (requires one of
122///    the `ring` or `aws-lc-rs` features).
123/// 2. Look for credentials in `.config/gcloud/application_default_credentials.json`;
124///    if found, use these credentials to request refresh tokens.
125/// 3. Send a HTTP request to the internal metadata server to retrieve a token;
126///    if it succeeds, use the default service account as the token source.
127/// 4. Check if the `gcloud` tool is available on the `PATH`; if so, use the
128///    `gcloud auth print-access-token` command as the token source.
129#[instrument(level = Level::DEBUG)]
130pub async fn provider() -> Result<Arc<dyn TokenProvider>, Error> {
131    debug!("initializing gcp_auth");
132    #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
133    if let Some(provider) = CustomServiceAccount::from_env()? {
134        return Ok(Arc::new(provider));
135    }
136
137    let client = HttpClient::new()?;
138    let default_user_error = match ConfigDefaultCredentials::with_client(&client).await {
139        Ok(provider) => {
140            debug!("using ConfigDefaultCredentials");
141            return Ok(Arc::new(provider));
142        }
143        Err(e) => e,
144    };
145
146    let default_service_error = match MetadataServiceAccount::with_client(&client).await {
147        Ok(provider) => {
148            debug!("using MetadataServiceAccount");
149            return Ok(Arc::new(provider));
150        }
151        Err(e) => e,
152    };
153
154    let gcloud_error = match GCloudAuthorizedUser::new().await {
155        Ok(provider) => {
156            debug!("using GCloudAuthorizedUser");
157            return Ok(Arc::new(provider));
158        }
159        Err(e) => e,
160    };
161
162    Err(Error::NoAuthMethod(
163        Box::new(gcloud_error),
164        Box::new(default_service_error),
165        Box::new(default_user_error),
166    ))
167}
168
169/// A trait for an authentication context that can provide tokens
170#[async_trait]
171pub trait TokenProvider: Send + Sync {
172    /// Get a valid token for the given scopes
173    ///
174    /// Tokens are cached until they expire, so this method will only fetch a fresh token once
175    /// the current token (for the given scopes) has expired.
176    async fn token(&self, scopes: &[&str]) -> Result<Arc<Token>, Error>;
177
178    /// Get the project ID for the authentication context
179    async fn project_id(&self) -> Result<Arc<str>, Error>;
180}
181
182/// Enumerates all possible errors returned by this library.
183#[derive(Error, Debug)]
184pub enum Error {
185    /// No available authentication method was discovered
186    ///
187    /// Application can authenticate against GCP using:
188    ///
189    /// - Default service account - available inside GCP platform using GCP Instance Metadata server
190    /// - GCloud authorized user - retrieved using `gcloud auth` command
191    ///
192    /// All authentication methods have been tested and none succeeded.
193    /// Service account file can be downloaded from GCP in json format.
194    #[error("no available authentication method found")]
195    NoAuthMethod(Box<Error>, Box<Error>, Box<Error>),
196
197    /// Could not connect to  server
198    #[error("{0}")]
199    Http(&'static str, #[source] hyper::Error),
200
201    #[error("{0}: {1}")]
202    Io(&'static str, #[source] std::io::Error),
203
204    #[error("{0}: {1}")]
205    Json(&'static str, #[source] serde_json::error::Error),
206
207    #[error("{0}: {1}")]
208    Other(
209        &'static str,
210        #[source] Box<dyn std::error::Error + Send + Sync>,
211    ),
212
213    #[error("{0}")]
214    Str(&'static str),
215}