1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! GCP auth provides authentication using service accounts Google Cloud Platform (GCP)
//!
//! GCP auth is a simple, minimal authentication library for Google Cloud Platform (GCP)
//! providing authentication using service accounts. Once authenticated, the service
//! account can be used to acquire bearer tokens for use in authenticating against GCP
//! services.
//!
//! The library supports the following methods of retrieving tokens:
//!
//! 1. Reading custom service account credentials from the path pointed to by the
//!    `GOOGLE_APPLICATION_CREDENTIALS` environment variable. Alternatively, custom service
//!    account credentials can be read from a JSON file or string.
//! 2. Look for credentials in `.config/gcloud/application_default_credentials.json`;
//!    if found, use these credentials to request refresh tokens. This file can be created
//!    by invoking `gcloud auth application-default login`.
//! 3. Use the default service account by retrieving a token from the metadata server.
//! 4. Retrieving a token from the `gcloud` CLI tool, if it is available on the `PATH`.
//!
//! For more details, see [`provider()`].
//!
//! A [`TokenProvider`] handles caching tokens for their lifetime; it will not make a request if
//! an appropriate token is already cached. Therefore, the caller should not cache tokens.
//!
//! ## Simple usage
//!
//! The default way to use this library is to select the appropriate token provider using
//! [`provider()`]. It will find the appropriate authentication method and use it to retrieve
//! tokens.
//!
//! ```rust,no_run
//! # async fn get_token() -> Result<(), gcp_auth::Error> {
//! let provider = gcp_auth::provider().await?;
//! let scopes = &["https://www.googleapis.com/auth/cloud-platform"];
//! let token = provider.token(scopes).await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Supplying service account credentials
//!
//! When running outside of GCP (for example, on a development machine), it can be useful to supply
//! service account credentials. The first method checked by [`provider()`] is to
//! read a path to a file containing JSON credentials in the `GOOGLE_APPLICATION_CREDENTIALS`
//! environment variable. However, you may also supply a custom path to read credentials from, or
//! a `&str` containing the credentials. In both of these cases, you should create a
//! [`CustomServiceAccount`] directly using one of its associated functions:
//!
//! ```rust,no_run
//! # use std::path::PathBuf;
//! #
//! # async fn get_token() -> Result<(), gcp_auth::Error> {
//! use gcp_auth::{CustomServiceAccount, TokenProvider};
//!
//! // `credentials_path` variable is the path for the credentials `.json` file.
//! let credentials_path = PathBuf::from("service-account.json");
//! let service_account = CustomServiceAccount::from_file(credentials_path)?;
//! let scopes = &["https://www.googleapis.com/auth/cloud-platform"];
//! let token = service_account.token(scopes).await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Getting tokens in multi-thread or async environments
//!
//! Using a `OnceCell` makes it easy to reuse the [`AuthenticationManager`] across different
//! threads or async tasks.
//!
//! ```rust,no_run
//! use std::sync::Arc;
//! use tokio::sync::OnceCell;
//! use gcp_auth::TokenProvider;
//!
//! static TOKEN_PROVIDER: OnceCell<Arc<dyn TokenProvider>> = OnceCell::const_new();
//!
//! async fn token_provider() -> &'static Arc<dyn TokenProvider> {
//!     TOKEN_PROVIDER
//!         .get_or_init(|| async {
//!             gcp_auth::provider()
//!                 .await
//!                 .expect("unable to initialize token provider")
//!         })
//!         .await
//! }
//! ```

#![warn(unreachable_pub)]

use std::sync::Arc;

use async_trait::async_trait;
use thiserror::Error;
use tracing::{debug, instrument, Level};

mod custom_service_account;
pub use custom_service_account::CustomServiceAccount;

mod config_default_credentials;
pub use config_default_credentials::ConfigDefaultCredentials;

mod metadata_service_account;
pub use metadata_service_account::MetadataServiceAccount;

mod gcloud_authorized_user;
pub use gcloud_authorized_user::GCloudAuthorizedUser;

mod types;
use types::HttpClient;
pub use types::{Signer, Token};

/// Finds a service account provider to get authentication tokens from
///
/// Tries the following approaches, in order:
///
/// 1. Check if the `GOOGLE_APPLICATION_CREDENTIALS` environment variable if set;
///    if so, use a custom service account as the token source.
/// 2. Look for credentials in `.config/gcloud/application_default_credentials.json`;
///    if found, use these credentials to request refresh tokens.
/// 3. Send a HTTP request to the internal metadata server to retrieve a token;
///    if it succeeds, use the default service account as the token source.
/// 4. Check if the `gcloud` tool is available on the `PATH`; if so, use the
///    `gcloud auth print-access-token` command as the token source.
#[instrument(level = Level::DEBUG)]
pub async fn provider() -> Result<Arc<dyn TokenProvider>, Error> {
    debug!("initializing gcp_auth");
    if let Some(provider) = CustomServiceAccount::from_env()? {
        return Ok(Arc::new(provider));
    }

    let client = HttpClient::new()?;
    let default_user_error = match ConfigDefaultCredentials::with_client(&client).await {
        Ok(provider) => {
            debug!("using ConfigDefaultCredentials");
            return Ok(Arc::new(provider));
        }
        Err(e) => e,
    };

    let default_service_error = match MetadataServiceAccount::with_client(&client).await {
        Ok(provider) => {
            debug!("using MetadataServiceAccount");
            return Ok(Arc::new(provider));
        }
        Err(e) => e,
    };

    let gcloud_error = match GCloudAuthorizedUser::new().await {
        Ok(provider) => {
            debug!("using GCloudAuthorizedUser");
            return Ok(Arc::new(provider));
        }
        Err(e) => e,
    };

    Err(Error::NoAuthMethod(
        Box::new(gcloud_error),
        Box::new(default_service_error),
        Box::new(default_user_error),
    ))
}

/// A trait for an authentication context that can provide tokens
#[async_trait]
pub trait TokenProvider: Send + Sync {
    /// Get a valid token for the given scopes
    ///
    /// Tokens are cached until they expire, so this method will only fetch a fresh token once
    /// the current token (for the given scopes) has expired.
    async fn token(&self, scopes: &[&str]) -> Result<Arc<Token>, Error>;

    /// Get the project ID for the authentication context
    async fn project_id(&self) -> Result<Arc<str>, Error>;
}

/// Enumerates all possible errors returned by this library.
#[derive(Error, Debug)]
pub enum Error {
    /// No available authentication method was discovered
    ///
    /// Application can authenticate against GCP using:
    ///
    /// - Default service account - available inside GCP platform using GCP Instance Metadata server
    /// - GCloud authorized user - retrieved using `gcloud auth` command
    ///
    /// All authentication methods have been tested and none succeeded.
    /// Service account file can be downloaded from GCP in json format.
    #[error("no available authentication method found")]
    NoAuthMethod(Box<Error>, Box<Error>, Box<Error>),

    /// Could not connect to  server
    #[error("{0}")]
    Http(&'static str, #[source] hyper::Error),

    #[error("{0}: {1}")]
    Io(&'static str, #[source] std::io::Error),

    #[error("{0}: {1}")]
    Json(&'static str, #[source] serde_json::error::Error),

    #[error("{0}: {1}")]
    Other(
        &'static str,
        #[source] Box<dyn std::error::Error + Send + Sync>,
    ),

    #[error("{0}")]
    Str(&'static str),
}