gcp_auth/
types.rs

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