Skip to main content

gcp_auth/
types.rs

1#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
2use std::env;
3use std::fmt;
4use std::fs::File;
5use std::path::Path;
6#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Duration;
10
11use bytes::Buf;
12use chrono::{DateTime, Utc};
13use http_body_util::{BodyExt, Full};
14use hyper::body::Bytes;
15use hyper::Request;
16use hyper_rustls::HttpsConnectorBuilder;
17use hyper_util::client::legacy::Client;
18use hyper_util::rt::TokioExecutor;
19use rustls::crypto::CryptoProvider;
20use serde::{Deserialize, Deserializer};
21use tokio::time::sleep;
22use tracing::{debug, warn};
23
24use crate::Error;
25
26#[derive(Clone, Debug)]
27pub(crate) struct HttpClient {
28    inner: Client<
29        hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
30        Full<Bytes>,
31    >,
32}
33
34impl HttpClient {
35    pub(crate) fn new() -> Result<Self, Error> {
36        #[cfg(feature = "webpki-roots")]
37        let https = HttpsConnectorBuilder::new()
38            .with_provider_and_webpki_roots(default_provider()?)
39            .map_err(|err| Error::Other("failed to initialize TLS configuration", Box::new(err)))?;
40        #[cfg(not(feature = "webpki-roots"))]
41        let https = HttpsConnectorBuilder::new()
42            .with_provider_and_native_roots(default_provider()?)
43            .map_err(|err| {
44                Error::Io("failed to load native TLS root certificates for HTTPS", err)
45            })?;
46
47        Ok(Self {
48            inner: Client::builder(TokioExecutor::new())
49                .build(https.https_or_http().enable_http2().build()),
50        })
51    }
52
53    pub(crate) async fn token(
54        &self,
55        request: &impl Fn() -> Request<Full<Bytes>>,
56        provider: &'static str,
57    ) -> Result<Arc<Token>, Error> {
58        //We multiply it by two on every iteration to progressively slow down ourself
59        //At most we will perform 50 + 100 + 200 + 400 wait as we're limited by 4 re-tries
60        let mut sleep_interval = Duration::from_millis(50);
61        let mut retries = 0;
62
63        let body = loop {
64            let err = match self.request(request(), provider).await {
65                // Early return when the request succeeds
66                Ok(body) => break body,
67                Err(err) => err,
68            };
69
70            warn!(
71                ?err,
72                provider, retries, "failed to refresh token, trying again..."
73            );
74
75            retries += 1;
76            if retries >= RETRY_COUNT {
77                return Err(err);
78            }
79
80            sleep(sleep_interval).await;
81            sleep_interval *= 2;
82        };
83
84        serde_json::from_slice(&body)
85            .map_err(|err| Error::Json("failed to deserialize token from response", err))
86    }
87
88    pub(crate) async fn request(
89        &self,
90        req: Request<Full<Bytes>>,
91        provider: &'static str,
92    ) -> Result<Bytes, Error> {
93        debug!(url = ?req.uri(), provider, "requesting token");
94        let (parts, body) = self
95            .inner
96            .request(req)
97            .await
98            .map_err(|err| Error::Other("HTTP request failed", Box::new(err)))?
99            .into_parts();
100
101        let mut body = body
102            .collect()
103            .await
104            .map_err(|err| Error::Http("failed to read HTTP response body", err))?
105            .aggregate();
106
107        let body = body.copy_to_bytes(body.remaining());
108        if !parts.status.is_success() {
109            let body = String::from_utf8_lossy(body.as_ref());
110            warn!(%body, status = ?parts.status, "token request failed");
111            return Err(Error::Str("token request failed"));
112        }
113
114        Ok(body)
115    }
116}
117
118/// Returns the bundled `ring` [`CryptoProvider`] backing the TLS configuration.
119#[cfg(feature = "ring")]
120fn default_provider() -> Result<Arc<CryptoProvider>, Error> {
121    Ok(Arc::new(rustls::crypto::ring::default_provider()))
122}
123
124/// Returns the bundled `aws-lc-rs` [`CryptoProvider`] backing the TLS configuration.
125#[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
126fn default_provider() -> Result<Arc<CryptoProvider>, Error> {
127    Ok(Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
128}
129
130/// Returns the per-process default [`CryptoProvider`] backing the TLS configuration.
131///
132/// With no crypto feature enabled, the provider is whatever the application has
133/// installed as the per-process default via [`CryptoProvider::install_default()`].
134#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))]
135fn default_provider() -> Result<Arc<CryptoProvider>, Error> {
136    CryptoProvider::get_default().cloned().ok_or(Error::Str(
137        "no per-process default crypto provider installed: enable the `ring` or `aws-lc-rs` \
138         feature, or install one with `CryptoProvider::install_default()`",
139    ))
140}
141
142/// Represents an access token that can be used as a bearer token in HTTP requests
143///
144/// Tokens should not be cached, the [`AuthenticationManager`] handles the correct caching
145/// already.
146///
147/// The token does not implement [`Display`] to avoid accidentally printing the token in log
148/// files, likewise [`Debug`] does not expose the token value itself which is only available
149/// using the [Token::`as_str`] method.
150///
151/// [`AuthenticationManager`]: crate::AuthenticationManager
152/// [`Display`]: fmt::Display
153/// Token data as returned by the server
154///
155/// https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token#response-body
156#[derive(Clone, Deserialize)]
157pub struct Token {
158    access_token: String,
159    #[serde(
160        deserialize_with = "deserialize_time",
161        rename(deserialize = "expires_in")
162    )]
163    expires_at: DateTime<Utc>,
164}
165
166impl Token {
167    pub(crate) fn from_string(access_token: String, expires_in: Duration) -> Self {
168        Token {
169            access_token,
170            expires_at: Utc::now() + expires_in,
171        }
172    }
173
174    /// Define if the token has has_expired
175    ///
176    /// This takes an additional 30s margin to ensure the token can still be reasonably used
177    /// instead of expiring right after having checked.
178    ///
179    /// Note:
180    /// The official Python implementation uses 20s and states it should be no more than 30s.
181    /// The official Go implementation uses 10s (0s for the metadata server).
182    /// The docs state, the metadata server caches tokens until 5 minutes before expiry.
183    /// We use 20s to be on the safe side.
184    pub fn has_expired(&self) -> bool {
185        self.expires_at - Duration::from_secs(20) <= Utc::now()
186    }
187
188    /// Get str representation of the token.
189    pub fn as_str(&self) -> &str {
190        &self.access_token
191    }
192
193    /// Get expiry of token, if available
194    pub fn expires_at(&self) -> DateTime<Utc> {
195        self.expires_at
196    }
197}
198
199impl fmt::Debug for Token {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.debug_struct("Token")
202            .field("access_token", &"****")
203            .field("expires_at", &self.expires_at)
204            .finish()
205    }
206}
207
208#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
209pub use self::sign::Signer;
210
211#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
212mod sign {
213    use std::fmt;
214
215    #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
216    use aws_lc_rs::rand::SystemRandom;
217    #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
218    use aws_lc_rs::signature::{KeyPair, RsaKeyPair, RSA_PKCS1_SHA256};
219    #[cfg(feature = "ring")]
220    use ring::rand::SystemRandom;
221    #[cfg(feature = "ring")]
222    use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
223    use rustls_pki_types::pem::PemObject;
224    use rustls_pki_types::PrivatePkcs8KeyDer;
225
226    use crate::Error;
227
228    /// An RSA PKCS1 SHA256 signer
229    ///
230    /// The signature is created by the enabled crypto provider: `ring` (the
231    /// default) or `aws-lc-rs`. `ring` takes precedence when both are enabled.
232    pub struct Signer {
233        key: RsaKeyPair,
234        rng: SystemRandom,
235    }
236
237    impl Signer {
238        pub(crate) fn new(pem_pkcs8: &str) -> Result<Self, Error> {
239            let key = match PrivatePkcs8KeyDer::from_pem_slice(pem_pkcs8.as_bytes()) {
240                Ok(key) => key,
241                Err(err) => {
242                    return Err(Error::Other(
243                        "failed to parse PKCS#8 RSA key pair",
244                        err.into(),
245                    ))
246                }
247            };
248
249            Ok(Signer {
250                key: RsaKeyPair::from_pkcs8(key.secret_pkcs8_der())
251                    .map_err(|_| Error::Str("invalid private key in credentials"))?,
252                rng: SystemRandom::new(),
253            })
254        }
255
256        /// Sign the input message and return the signature
257        pub fn sign(&self, input: &[u8]) -> Result<Vec<u8>, Error> {
258            #[cfg(feature = "ring")]
259            let modulus_len = self.key.public().modulus_len();
260            #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
261            let modulus_len = self.key.public_key().modulus_len();
262
263            let mut signature = vec![0; modulus_len];
264            self.key
265                .sign(&RSA_PKCS1_SHA256, &self.rng, input, &mut signature)
266                .map_err(|_| Error::Str("failed to sign with credentials key"))?;
267            Ok(signature)
268        }
269    }
270
271    impl fmt::Debug for Signer {
272        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273            f.debug_struct("Signer").finish()
274        }
275    }
276}
277
278fn deserialize_time<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
279where
280    D: Deserializer<'de>,
281{
282    let seconds_from_now: u64 = Deserialize::deserialize(deserializer)?;
283    Ok(Utc::now() + Duration::from_secs(seconds_from_now))
284}
285
286#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
287#[derive(Deserialize)]
288pub(crate) struct ServiceAccountKey {
289    /// project_id
290    pub(crate) project_id: Option<Arc<str>>,
291    /// private_key
292    pub(crate) private_key: String,
293    /// client_email
294    pub(crate) client_email: String,
295    /// token_uri
296    pub(crate) token_uri: String,
297}
298
299#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
300impl ServiceAccountKey {
301    pub(crate) fn from_env() -> Result<Option<Self>, Error> {
302        env::var_os("GOOGLE_APPLICATION_CREDENTIALS")
303            .map(|path| {
304                debug!(
305                    ?path,
306                    "reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"
307                );
308                Self::from_file(&path)
309            })
310            .transpose()
311    }
312
313    pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
314        let file = File::open(path.as_ref())
315            .map_err(|err| Error::Io("failed to open application credentials file", err))?;
316        serde_json::from_reader(file)
317            .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
318    }
319}
320
321#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
322impl FromStr for ServiceAccountKey {
323    type Err = Error;
324
325    fn from_str(s: &str) -> Result<Self, Self::Err> {
326        serde_json::from_str(s)
327            .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
328    }
329}
330
331#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
332impl fmt::Debug for ServiceAccountKey {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        f.debug_struct("ApplicationCredentials")
335            .field("client_email", &self.client_email)
336            .field("project_id", &self.project_id)
337            .finish_non_exhaustive()
338    }
339}
340
341#[derive(Deserialize)]
342pub(crate) struct AuthorizedUserRefreshToken {
343    /// Client id
344    pub(crate) client_id: String,
345    /// Client secret
346    pub(crate) client_secret: String,
347    /// Project ID
348    pub(crate) quota_project_id: Option<Arc<str>>,
349    /// Refresh Token
350    pub(crate) refresh_token: String,
351}
352
353impl AuthorizedUserRefreshToken {
354    pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
355        let file = File::open(path.as_ref())
356            .map_err(|err| Error::Io("failed to open application credentials file", err))?;
357        serde_json::from_reader(file)
358            .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
359    }
360}
361
362impl fmt::Debug for AuthorizedUserRefreshToken {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.debug_struct("UserCredentials")
365            .field("client_id", &self.client_id)
366            .field("quota_project_id", &self.quota_project_id)
367            .finish_non_exhaustive()
368    }
369}
370
371/// How many times to attempt to fetch a token from the set credentials token endpoint.
372const RETRY_COUNT: u8 = 5;
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_deserialize_with_time() {
380        let s = r#"{"access_token":"abc123","expires_in":100}"#;
381        let token: Token = serde_json::from_str(s).unwrap();
382        let expires = Utc::now() + Duration::from_secs(100);
383
384        assert_eq!(token.as_str(), "abc123");
385
386        // Testing time is always racy, give it 1s leeway.
387        let expires_at = token.expires_at();
388        assert!(expires_at < expires + Duration::from_secs(1));
389        assert!(expires_at > expires - Duration::from_secs(1));
390    }
391}