Skip to main content

object_store/gcp/
credential.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use super::client::GoogleCloudStorageClient;
19use crate::client::builder::HttpRequestBuilder;
20use crate::client::retry::RetryExt;
21use crate::client::token::TemporaryToken;
22use crate::client::{
23    CryptoProvider, HttpClient, HttpError, Signer, SigningAlgorithm, TokenProvider,
24};
25use crate::gcp::{GcpSigningCredentialProvider, STORE};
26use crate::util::{STRICT_ENCODE_SET, append_strict_query_pairs, hex_digest, hex_encode};
27use crate::{RetryConfig, StaticCredentialProvider};
28use async_trait::async_trait;
29use base64::Engine;
30use base64::prelude::BASE64_URL_SAFE_NO_PAD;
31use chrono::{DateTime, Utc};
32use futures_util::TryFutureExt;
33use http::{HeaderMap, Method};
34use itertools::Itertools;
35use percent_encoding::utf8_percent_encode;
36use serde::Deserialize;
37use std::collections::BTreeMap;
38use std::env;
39use std::fs::File;
40use std::io::BufReader;
41use std::path::{Path, PathBuf};
42use std::sync::Arc;
43use std::time::{Duration, Instant};
44use tracing::info;
45use url::Url;
46
47pub(crate) const DEFAULT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
48
49pub(crate) const DEFAULT_GCS_BASE_URL: &str = "https://storage.googleapis.com";
50
51const DEFAULT_GCS_PLAYLOAD_STRING: &str = "UNSIGNED-PAYLOAD";
52const DEFAULT_GCS_SIGN_BLOB_HOST: &str = "storage.googleapis.com";
53
54const DEFAULT_METADATA_HOST: &str = "metadata.google.internal";
55const DEFAULT_METADATA_IP: &str = "169.254.169.254";
56
57#[derive(Debug, thiserror::Error)]
58pub(super) enum Error {
59    #[error("Unable to open service account file from {}: {}", path.display(), source)]
60    OpenCredentials {
61        source: std::io::Error,
62        path: PathBuf,
63    },
64
65    #[error("Unable to decode service account file: {}", source)]
66    DecodeCredentials { source: serde_json::Error },
67
68    #[error("Error encoding jwt payload: {}", source)]
69    Encode { source: serde_json::Error },
70
71    #[error("Error performing token request: {}", source)]
72    TokenRequest {
73        source: crate::client::retry::RetryError,
74    },
75
76    #[error("Error getting token response body: {}", source)]
77    TokenResponseBody { source: HttpError },
78}
79
80impl From<Error> for crate::Error {
81    fn from(value: Error) -> Self {
82        Self::Generic {
83            store: STORE,
84            source: Box::new(value),
85        }
86    }
87}
88
89/// A Google Cloud Storage Credential for signing
90#[derive(Debug)]
91pub struct GcpSigningCredential {
92    /// The email of the service account
93    pub email: String,
94
95    /// An optional RSA private key
96    ///
97    /// If provided this will be used to sign the URL, otherwise a call will be made to
98    /// [`iam.serviceAccounts.signBlob`]. This allows supporting credential sources
99    /// that don't expose the service account private key, e.g. [IMDS].
100    ///
101    /// [IMDS]: https://cloud.google.com/docs/authentication/get-id-token#metadata-server
102    /// [`iam.serviceAccounts.signBlob`]: https://cloud.google.com/storage/docs/authentication/creating-signatures
103    pub private_key: Option<ServiceAccountKey>,
104}
105
106/// A private RSA key for a service account
107pub struct ServiceAccountKey(Box<dyn Signer>);
108
109impl std::fmt::Debug for ServiceAccountKey {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_tuple("ServiceAccountKey").finish_non_exhaustive()
112    }
113}
114
115impl ServiceAccountKey {
116    /// Creates a [`ServiceAccountKey`] from the provided [`Signer`]
117    pub fn new(signer: Box<dyn Signer>) -> Self {
118        Self(signer)
119    }
120
121    /// Parses a pem-encoded RSA key
122    #[cfg(feature = "aws-lc-rs")]
123    pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
124        let key = crate::client::aws_lc_rs::RsaKeyPair::from_pem(encoded)?;
125        Ok(Self::new(Box::new(key)))
126    }
127
128    /// Parses an unencrypted PKCS#8-encoded RSA private key.
129    #[cfg(feature = "aws-lc-rs")]
130    pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
131        let key = crate::client::aws_lc_rs::RsaKeyPair::from_pkcs8(key)?;
132        Ok(Self::new(Box::new(key)))
133    }
134
135    /// Parses an unencrypted PKCS#8-encoded RSA private key.
136    #[cfg(feature = "aws-lc-rs")]
137    pub fn from_der(key: &[u8]) -> crate::Result<Self> {
138        let key = crate::client::aws_lc_rs::RsaKeyPair::from_der(key)?;
139        Ok(Self::new(Box::new(key)))
140    }
141
142    /// Parses a pem-encoded RSA key
143    #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
144    pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
145        let key = crate::client::ring::RsaKeyPair::from_pem(encoded)?;
146        Ok(Self::new(Box::new(key)))
147    }
148
149    /// Parses an unencrypted PKCS#8-encoded RSA private key.
150    #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
151    pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
152        let key = crate::client::ring::RsaKeyPair::from_pkcs8(key)?;
153        Ok(Self::new(Box::new(key)))
154    }
155
156    /// Parses an unencrypted PKCS#8-encoded RSA private key.
157    #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
158    pub fn from_der(key: &[u8]) -> crate::Result<Self> {
159        let key = crate::client::ring::RsaKeyPair::from_der(key)?;
160        Ok(Self::new(Box::new(key)))
161    }
162
163    fn sign(&self, string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
164        self.0.sign(string_to_sign)
165    }
166}
167
168/// A Google Cloud Storage Credential
169#[derive(Debug, Eq, PartialEq)]
170pub struct GcpCredential {
171    /// An HTTP bearer token
172    pub bearer: String,
173}
174
175pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
176
177#[derive(Debug, Default, serde::Serialize)]
178pub(crate) struct JwtHeader<'a> {
179    /// The type of JWS: it can only be "JWT" here
180    ///
181    /// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9).
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub typ: Option<&'a str>,
184    /// The algorithm used
185    ///
186    /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1).
187    pub alg: &'a str,
188    /// Content type
189    ///
190    /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2).
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub cty: Option<&'a str>,
193    /// JSON Key URL
194    ///
195    /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2).
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub jku: Option<&'a str>,
198    /// Key ID
199    ///
200    /// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub kid: Option<&'a str>,
203    /// X.509 URL
204    ///
205    /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5).
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub x5u: Option<&'a str>,
208    /// X.509 certificate thumbprint
209    ///
210    /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7).
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub x5t: Option<&'a str>,
213}
214
215#[derive(serde::Serialize)]
216struct TokenClaims<'a> {
217    iss: &'a str,
218    sub: &'a str,
219    scope: &'a str,
220    exp: u64,
221    iat: u64,
222}
223
224#[derive(serde::Deserialize, Debug)]
225struct TokenResponse {
226    access_token: String,
227    expires_in: u64,
228    id_token: Option<String>,
229}
230
231/// Self-signed JWT (JSON Web Token).
232///
233/// # References
234/// - <https://google.aip.dev/auth/4111>
235#[derive(Debug)]
236pub(crate) struct SelfSignedJwt {
237    issuer: String,
238    scope: String,
239    private_key: ServiceAccountKey,
240    key_id: String,
241}
242
243impl SelfSignedJwt {
244    /// Create a new [`SelfSignedJwt`]
245    pub(crate) fn new(
246        key_id: String,
247        issuer: String,
248        private_key: ServiceAccountKey,
249        scope: String,
250    ) -> Result<Self> {
251        Ok(Self {
252            issuer,
253            scope,
254            private_key,
255            key_id,
256        })
257    }
258}
259
260#[async_trait]
261impl TokenProvider for SelfSignedJwt {
262    type Credential = GcpCredential;
263
264    /// Fetch a fresh token
265    async fn fetch_token(
266        &self,
267        _client: &HttpClient,
268        _retry: &RetryConfig,
269    ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
270        let now = seconds_since_epoch();
271        let exp = now + 3600;
272
273        let claims = TokenClaims {
274            iss: &self.issuer,
275            sub: &self.issuer,
276            scope: &self.scope,
277            iat: now,
278            exp,
279        };
280
281        let jwt_header = b64_encode_obj(&JwtHeader {
282            alg: "RS256",
283            typ: Some("JWT"),
284            kid: Some(&self.key_id),
285            ..Default::default()
286        })?;
287
288        let claim_str = b64_encode_obj(&claims)?;
289        let message = [jwt_header.as_ref(), claim_str.as_ref()].join(".");
290        let sig_bytes = self.private_key.sign(message.as_bytes())?;
291        let signature = BASE64_URL_SAFE_NO_PAD.encode(sig_bytes);
292        let bearer = [message, signature].join(".");
293
294        Ok(TemporaryToken {
295            token: Arc::new(GcpCredential { bearer }),
296            expiry: Some(Instant::now() + Duration::from_secs(3600)),
297        })
298    }
299}
300
301fn read_credentials_file<T>(service_account_path: impl AsRef<std::path::Path>) -> Result<T>
302where
303    T: serde::de::DeserializeOwned,
304{
305    let file = File::open(&service_account_path).map_err(|source| {
306        let path = service_account_path.as_ref().to_owned();
307        Error::OpenCredentials { source, path }
308    })?;
309    let reader = BufReader::new(file);
310    serde_json::from_reader(reader).map_err(|source| Error::DecodeCredentials { source })
311}
312
313/// A deserialized `service-account-********.json`-file.
314#[derive(serde::Deserialize, Debug, Clone)]
315pub(crate) struct ServiceAccountCredentials {
316    /// The private key in RSA format.
317    pub private_key: String,
318
319    /// The private key ID
320    pub private_key_id: String,
321
322    /// The email address associated with the service account.
323    pub client_email: String,
324
325    /// Base URL for GCS
326    #[serde(default)]
327    pub gcs_base_url: Option<String>,
328
329    /// Disable oauth and use empty tokens.
330    #[serde(default)]
331    pub disable_oauth: bool,
332}
333
334impl ServiceAccountCredentials {
335    /// Create a new [`ServiceAccountCredentials`] from a file.
336    pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
337        read_credentials_file(path)
338    }
339
340    /// Create a new [`ServiceAccountCredentials`] from a string.
341    pub(crate) fn from_key(key: &str) -> Result<Self> {
342        serde_json::from_str(key).map_err(|source| Error::DecodeCredentials { source })
343    }
344
345    /// Create a [`SelfSignedJwt`] from this credentials struct.
346    ///
347    /// We use a scope of [`DEFAULT_SCOPE`] as opposed to an audience
348    /// as GCS appears to not support audience
349    ///
350    /// # References
351    /// - <https://stackoverflow.com/questions/63222450/service-account-authorization-without-oauth-can-we-get-file-from-google-cloud/71834557#71834557>
352    /// - <https://www.codejam.info/2022/05/google-cloud-service-account-authorization-without-oauth.html>
353    pub(crate) fn token_provider(
354        self,
355        crypto: &dyn CryptoProvider,
356    ) -> crate::Result<SelfSignedJwt> {
357        let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?;
358        Ok(SelfSignedJwt::new(
359            self.private_key_id,
360            self.client_email,
361            ServiceAccountKey::new(key),
362            DEFAULT_SCOPE.to_string(),
363        )?)
364    }
365
366    pub(crate) fn signing_credentials(
367        self,
368        crypto: &dyn CryptoProvider,
369    ) -> crate::Result<GcpSigningCredentialProvider> {
370        let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?;
371        Ok(Arc::new(StaticCredentialProvider::new(
372            GcpSigningCredential {
373                email: self.client_email,
374                private_key: Some(ServiceAccountKey::new(key)),
375            },
376        )))
377    }
378}
379
380/// Returns the number of seconds since unix epoch
381fn seconds_since_epoch() -> u64 {
382    std::time::SystemTime::now()
383        .duration_since(std::time::SystemTime::UNIX_EPOCH)
384        .unwrap()
385        .as_secs()
386}
387
388fn b64_encode_obj<T: serde::Serialize>(obj: &T) -> Result<String> {
389    let string = serde_json::to_string(obj).map_err(|source| Error::Encode { source })?;
390    Ok(BASE64_URL_SAFE_NO_PAD.encode(string))
391}
392
393/// A provider that uses the Google Cloud Platform metadata server to fetch a token.
394///
395/// <https://cloud.google.com/docs/authentication/get-id-token#metadata-server>
396#[derive(Debug, Default)]
397pub(crate) struct InstanceCredentialProvider {}
398
399/// Make a request to the metadata server to fetch a token, using a a given hostname.
400async fn make_metadata_request(
401    client: &HttpClient,
402    hostname: &str,
403    retry: &RetryConfig,
404) -> crate::Result<TokenResponse> {
405    let url =
406        format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/token");
407    let response: TokenResponse = client
408        .get(url)
409        .header("Metadata-Flavor", "Google")
410        .query(&[("audience", "https://www.googleapis.com/oauth2/v4/token")])
411        .send_retry(retry)
412        .await
413        .map_err(|source| Error::TokenRequest { source })?
414        .into_body()
415        .json()
416        .await
417        .map_err(|source| Error::TokenResponseBody { source })?;
418    Ok(response)
419}
420
421#[async_trait]
422impl TokenProvider for InstanceCredentialProvider {
423    type Credential = GcpCredential;
424
425    /// Fetch a token from the metadata server.
426    /// Since the connection is local we need to enable http access and don't actually use the client object passed in.
427    /// Respects the `GCE_METADATA_HOST`, `GCE_METADATA_ROOT`, and `GCE_METADATA_IP`
428    /// environment variables.
429    ///
430    /// References: <https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html>
431    async fn fetch_token(
432        &self,
433        client: &HttpClient,
434        retry: &RetryConfig,
435    ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
436        let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
437            host
438        } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
439            host
440        } else {
441            DEFAULT_METADATA_HOST.to_string()
442        };
443        let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
444            ip
445        } else {
446            DEFAULT_METADATA_IP.to_string()
447        };
448
449        info!("fetching token from metadata server");
450        let response = make_metadata_request(client, &metadata_host, retry)
451            .or_else(|_| make_metadata_request(client, &metadata_ip, retry))
452            .await?;
453
454        let token = TemporaryToken {
455            token: Arc::new(GcpCredential {
456                bearer: response.access_token,
457            }),
458            expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
459        };
460        Ok(token)
461    }
462}
463
464/// Make a request to the metadata server to fetch the client email, using a given hostname.
465async fn make_metadata_request_for_email(
466    client: &HttpClient,
467    hostname: &str,
468    retry: &RetryConfig,
469) -> crate::Result<String> {
470    let url =
471        format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/email",);
472    let response = client
473        .get(url)
474        .header("Metadata-Flavor", "Google")
475        .send_retry(retry)
476        .await
477        .map_err(|source| Error::TokenRequest { source })?
478        .into_body()
479        .text()
480        .await
481        .map_err(|source| Error::TokenResponseBody { source })?;
482    Ok(response)
483}
484
485/// A provider that uses the Google Cloud Platform metadata server to fetch a email for signing.
486///
487/// <https://cloud.google.com/appengine/docs/legacy/standard/java/accessing-instance-metadata>
488#[derive(Debug, Default)]
489pub(crate) struct InstanceSigningCredentialProvider {}
490
491#[async_trait]
492impl TokenProvider for InstanceSigningCredentialProvider {
493    type Credential = GcpSigningCredential;
494
495    /// Fetch a token from the metadata server.
496    /// Since the connection is local we need to enable http access and don't actually use the client object passed in.
497    /// Respects the `GCE_METADATA_HOST`, `GCE_METADATA_ROOT`, and `GCE_METADATA_IP`
498    /// environment variables.
499    ///
500    /// References: <https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html>
501    async fn fetch_token(
502        &self,
503        client: &HttpClient,
504        retry: &RetryConfig,
505    ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
506        let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
507            host
508        } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
509            host
510        } else {
511            DEFAULT_METADATA_HOST.to_string()
512        };
513
514        let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
515            ip
516        } else {
517            DEFAULT_METADATA_IP.to_string()
518        };
519
520        info!("fetching token from metadata server");
521
522        let email = make_metadata_request_for_email(client, &metadata_host, retry)
523            .or_else(|_| make_metadata_request_for_email(client, &metadata_ip, retry))
524            .await?;
525
526        let token = TemporaryToken {
527            token: Arc::new(GcpSigningCredential {
528                email,
529                private_key: None,
530            }),
531            expiry: None,
532        };
533        Ok(token)
534    }
535}
536
537/// A deserialized `application_default_credentials.json`-file.
538///
539/// # References
540/// - <https://cloud.google.com/docs/authentication/application-default-credentials#personal>
541/// - <https://google.aip.dev/auth/4110>
542#[derive(serde::Deserialize, Clone)]
543#[serde(tag = "type")]
544pub(crate) enum ApplicationDefaultCredentials {
545    /// Service Account.
546    ///
547    /// # References
548    /// - <https://google.aip.dev/auth/4112>
549    #[serde(rename = "service_account")]
550    ServiceAccount(ServiceAccountCredentials),
551    /// Authorized user via "gcloud CLI Integration".
552    ///
553    /// # References
554    /// - <https://google.aip.dev/auth/4113>
555    #[serde(rename = "authorized_user")]
556    AuthorizedUser(AuthorizedUserCredentials),
557}
558
559impl ApplicationDefaultCredentials {
560    const CREDENTIALS_PATH: &'static str = if cfg!(windows) {
561        "gcloud/application_default_credentials.json"
562    } else {
563        ".config/gcloud/application_default_credentials.json"
564    };
565
566    // Create a new application default credential in the following situations:
567    //  1. a file is passed in and the type matches.
568    //  2. without argument if the well-known configuration file is present.
569    pub(crate) fn read(path: Option<&str>) -> Result<Option<Self>, Error> {
570        if let Some(path) = path {
571            return read_credentials_file::<Self>(path).map(Some);
572        }
573
574        let home_var = if cfg!(windows) { "APPDATA" } else { "HOME" };
575        if let Some(home) = env::var_os(home_var) {
576            let path = Path::new(&home).join(Self::CREDENTIALS_PATH);
577
578            // It's expected for this file to not exist unless it has been explicitly configured by the user.
579            if path.exists() {
580                return read_credentials_file::<Self>(path).map(Some);
581            }
582        }
583        Ok(None)
584    }
585}
586
587const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
588
589/// <https://google.aip.dev/auth/4113>
590#[derive(Debug, Deserialize, Clone)]
591pub(crate) struct AuthorizedUserCredentials {
592    client_id: String,
593    client_secret: String,
594    refresh_token: String,
595}
596
597#[derive(Debug, Deserialize)]
598pub(crate) struct AuthorizedUserSigningCredentials {
599    credential: AuthorizedUserCredentials,
600}
601
602///<https://oauth2.googleapis.com/tokeninfo?access_token=ACCESS_TOKEN>
603#[derive(Debug, Deserialize)]
604struct EmailResponse {
605    email: String,
606}
607
608#[derive(Debug, Deserialize)]
609struct IdTokenClaims {
610    email: String,
611}
612
613async fn get_token_response(
614    client_id: &str,
615    client_secret: &str,
616    refresh_token: &str,
617    client: &HttpClient,
618    retry: &RetryConfig,
619) -> Result<TokenResponse> {
620    client
621        .post(DEFAULT_TOKEN_GCP_URI)
622        .form([
623            ("grant_type", "refresh_token"),
624            ("client_id", client_id),
625            ("client_secret", client_secret),
626            ("refresh_token", refresh_token),
627        ])
628        .retryable(retry)
629        .idempotent(true)
630        .send()
631        .await
632        .map_err(|source| Error::TokenRequest { source })?
633        .into_body()
634        .json::<TokenResponse>()
635        .await
636        .map_err(|source| Error::TokenResponseBody { source })
637}
638
639impl AuthorizedUserSigningCredentials {
640    pub(crate) fn from(credential: AuthorizedUserCredentials) -> crate::Result<Self> {
641        Ok(Self { credential })
642    }
643
644    async fn client_email(
645        &self,
646        client: &HttpClient,
647        retry: &RetryConfig,
648    ) -> crate::Result<String> {
649        let response = get_token_response(
650            &self.credential.client_id,
651            &self.credential.client_secret,
652            &self.credential.refresh_token,
653            client,
654            retry,
655        )
656        .await?;
657
658        // Extract email from id_token if available
659        if let Some(id_token) = response.id_token {
660            // Split the JWT string by dots to get the payload section
661            let parts: Vec<&str> = id_token.split('.').collect();
662            if parts.len() == 3 {
663                // Decode the base64-encoded payload (middle part)
664                if let Ok(payload) = BASE64_URL_SAFE_NO_PAD.decode(parts[1]) {
665                    // Parse the payload as JSON and extract the email
666                    if let Ok(claims) = serde_json::from_slice::<IdTokenClaims>(&payload) {
667                        return Ok(claims.email);
668                    }
669                }
670                // If any of the parsing steps fail, fallback to other method
671            }
672        }
673
674        // Fallback to the original method if id_token is not available or invalid
675        let response = client
676            .get("https://oauth2.googleapis.com/tokeninfo")
677            .query(&[("access_token", response.access_token)])
678            .send_retry(retry)
679            .await
680            .map_err(|source| Error::TokenRequest { source })?
681            .into_body()
682            .json::<EmailResponse>()
683            .await
684            .map_err(|source: HttpError| Error::TokenResponseBody { source })?;
685
686        Ok(response.email)
687    }
688}
689
690#[async_trait]
691impl TokenProvider for AuthorizedUserSigningCredentials {
692    type Credential = GcpSigningCredential;
693
694    async fn fetch_token(
695        &self,
696        client: &HttpClient,
697        retry: &RetryConfig,
698    ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
699        let email = self.client_email(client, retry).await?;
700        Ok(TemporaryToken {
701            token: Arc::new(GcpSigningCredential {
702                email,
703                private_key: None,
704            }),
705            expiry: None,
706        })
707    }
708}
709
710#[async_trait]
711impl TokenProvider for AuthorizedUserCredentials {
712    type Credential = GcpCredential;
713
714    async fn fetch_token(
715        &self,
716        client: &HttpClient,
717        retry: &RetryConfig,
718    ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
719        let response = get_token_response(
720            &self.client_id,
721            &self.client_secret,
722            &self.refresh_token,
723            client,
724            retry,
725        )
726        .await?;
727
728        Ok(TemporaryToken {
729            token: Arc::new(GcpCredential {
730                bearer: response.access_token,
731            }),
732            expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
733        })
734    }
735}
736
737/// Normalize a header value for the canonical request: trim leading/trailing whitespace and
738/// collapse runs of internal whitespace to a single space, as required by the canonical request
739/// specification.
740///
741/// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-headers>
742fn trim_header_value(value: &str) -> String {
743    value.split_whitespace().collect::<Vec<_>>().join(" ")
744}
745
746/// A Google Cloud Storage Authorizer for generating signed URL using [Google SigV4]
747///
748/// [Google SigV4]: https://cloud.google.com/storage/docs/access-control/signed-urls
749#[derive(Debug)]
750pub(crate) struct GCSAuthorizer {
751    date: Option<DateTime<Utc>>,
752    credential: Arc<GcpSigningCredential>,
753}
754
755impl GCSAuthorizer {
756    /// Create a new [`GCSAuthorizer`]
757    pub(crate) fn new(credential: Arc<GcpSigningCredential>) -> Self {
758        Self {
759            date: None,
760            credential,
761        }
762    }
763
764    /// Generate a signed URL, additionally folding `extra_query` parameters and `signed_headers`
765    /// into the [GOOG4 signed URLs] signature.
766    ///
767    /// `extra_query` lets callers sign query parameters the recipient must send (e.g. an upload
768    /// `partNumber`/`uploadId`), and `signed_headers` binds request headers (e.g. `content-type`)
769    /// to the signature. For a signed URL these values are fixed at signing time. The mandatory
770    /// `host` header is always signed.
771    ///
772    /// [GOOG4 signed URLs]: https://cloud.google.com/storage/docs/access-control/signed-urls
773    #[allow(clippy::too_many_arguments)]
774    pub(crate) async fn sign_with(
775        &self,
776        crypto: &dyn CryptoProvider,
777        method: Method,
778        url: &mut Url,
779        extra_query: &[(String, String)],
780        signed_headers: &HeaderMap,
781        expires_in: Duration,
782        client: &GoogleCloudStorageClient,
783    ) -> crate::Result<()> {
784        let email = &self.credential.email;
785        let date = self.date.unwrap_or_else(Utc::now);
786        let scope = self.scope(date);
787        let credential_with_scope = format!("{email}/{scope}");
788
789        // Append any caller-provided query parameters before signing so they are folded into
790        // the canonical query string and become part of the signature.
791        append_strict_query_pairs(url, extra_query);
792
793        // The `host` header is always signed; callers may bind additional headers whose values
794        // are committed to the signature at signing time.
795        let mut headers = HeaderMap::with_capacity(1 + signed_headers.len());
796        headers.insert("host", DEFAULT_GCS_SIGN_BLOB_HOST.parse().unwrap());
797        for (name, value) in signed_headers {
798            headers.append(name.clone(), value.clone());
799        }
800
801        let (_, signed_headers) = Self::canonicalize_headers(&headers);
802
803        url.query_pairs_mut()
804            .append_pair("X-Goog-Algorithm", "GOOG4-RSA-SHA256")
805            .append_pair("X-Goog-Credential", &credential_with_scope)
806            .append_pair("X-Goog-Date", &date.format("%Y%m%dT%H%M%SZ").to_string())
807            .append_pair("X-Goog-Expires", &expires_in.as_secs().to_string())
808            .append_pair("X-Goog-SignedHeaders", &signed_headers);
809
810        let string_to_sign = self.string_to_sign(crypto, date, &method, url, &headers)?;
811        let signature = match &self.credential.private_key {
812            Some(key) => hex_encode(&key.sign(string_to_sign.as_bytes())?),
813            None => client.sign_blob(&string_to_sign, email).await?,
814        };
815
816        url.query_pairs_mut()
817            .append_pair("X-Goog-Signature", &signature);
818        Ok(())
819    }
820
821    /// Get scope for the request
822    ///
823    /// <https://cloud.google.com/storage/docs/authentication/signatures#credential-scope>
824    fn scope(&self, date: DateTime<Utc>) -> String {
825        format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
826    }
827
828    /// Canonicalizes query parameters into the GCP canonical form
829    /// form like:
830    ///```plaintext
831    ///HTTP_VERB
832    ///PATH_TO_RESOURCE
833    ///CANONICAL_QUERY_STRING
834    ///CANONICAL_HEADERS
835    ///
836    ///SIGNED_HEADERS
837    ///PAYLOAD
838    ///```
839    ///
840    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests>
841    fn canonicalize_request(url: &Url, method: &Method, headers: &HeaderMap) -> String {
842        let verb = method.as_str();
843        let path = url.path();
844        let query = Self::canonicalize_query(url);
845        let (canonical_headers, signed_headers) = Self::canonicalize_headers(headers);
846
847        format!(
848            "{verb}\n{path}\n{query}\n{canonical_headers}\n\n{signed_headers}\n{DEFAULT_GCS_PLAYLOAD_STRING}"
849        )
850    }
851
852    /// Canonicalizes query parameters into the GCP canonical form
853    /// form like `max-keys=2&prefix=object`
854    ///
855    /// Parameters are sorted by encoded name, with ties broken by encoded value, as required by
856    /// the canonical request specification — sorting by the decoded name (or ignoring the value
857    /// for duplicate names) can order parameters differently from how the server verifies them.
858    ///
859    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-query-strings>
860    fn canonicalize_query(url: &Url) -> String {
861        url.query_pairs()
862            .map(|(k, v)| {
863                (
864                    utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET).to_string(),
865                    utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET).to_string(),
866                )
867            })
868            .sorted_unstable()
869            .map(|(k, v)| format!("{k}={v}"))
870            .join("&")
871    }
872
873    /// Canonicalizes header into the GCP canonical form
874    ///
875    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-headers>
876    fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
877        //FIXME add error handling for invalid header values
878        let mut headers = BTreeMap::<String, Vec<&str>>::new();
879        for (k, v) in header_map {
880            headers
881                .entry(k.as_str().to_lowercase())
882                .or_default()
883                .push(std::str::from_utf8(v.as_bytes()).unwrap());
884        }
885
886        let canonicalize_headers = headers
887            .iter()
888            .map(|(k, v)| {
889                format!(
890                    "{}:{}",
891                    k.trim(),
892                    v.iter().map(|v| trim_header_value(v)).join(",")
893                )
894            })
895            .join("\n");
896
897        let signed_headers = headers.keys().join(";");
898
899        (canonicalize_headers, signed_headers)
900    }
901
902    ///construct the string to sign
903    ///form like:
904    ///```plaintext
905    ///SIGNING_ALGORITHM
906    ///ACTIVE_DATETIME
907    ///CREDENTIAL_SCOPE
908    ///HASHED_CANONICAL_REQUEST
909    ///```
910    ///`ACTIVE_DATETIME` format:`YYYYMMDD'T'HHMMSS'Z'`
911    /// <https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign>
912    pub(crate) fn string_to_sign(
913        &self,
914        crypto: &dyn CryptoProvider,
915        date: DateTime<Utc>,
916        request_method: &Method,
917        url: &Url,
918        headers: &HeaderMap,
919    ) -> crate::Result<String> {
920        let canonical_request = Self::canonicalize_request(url, request_method, headers);
921        let hashed_canonical_req = hex_digest(crypto, canonical_request.as_bytes())?;
922        let scope = self.scope(date);
923
924        Ok(format!(
925            "{}\n{}\n{}\n{}",
926            "GOOG4-RSA-SHA256",
927            date.format("%Y%m%dT%H%M%SZ"),
928            scope,
929            hashed_canonical_req
930        ))
931    }
932}
933
934pub(crate) trait CredentialExt {
935    /// Apply bearer authentication to the request if the credential is not None
936    fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self;
937}
938
939impl CredentialExt for HttpRequestBuilder {
940    fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self {
941        match credential {
942            Some(credential) => {
943                if credential.bearer.is_empty() {
944                    self
945                } else {
946                    self.bearer_auth(&credential.bearer)
947                }
948            }
949            None => self,
950        }
951    }
952}
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957    use crate::client::{
958        ClientOptions, DigestAlgorithm, DigestContext, HmacContext, StaticCredentialProvider,
959    };
960    use crate::gcp::client::{GoogleCloudStorageClient, GoogleCloudStorageConfig};
961    use http::{HeaderName, HeaderValue};
962
963    const SIGNATURE_BYTES: &[u8] = &[0x00, 0x01, 0x02, 0xab, 0xcd];
964
965    struct FixedSigner;
966
967    impl Signer for FixedSigner {
968        fn sign(&self, _string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
969            Ok(SIGNATURE_BYTES.to_vec())
970        }
971    }
972
973    #[derive(Debug)]
974    struct FixedCryptoProvider;
975
976    impl CryptoProvider for FixedCryptoProvider {
977        fn digest(&self, _algorithm: DigestAlgorithm) -> crate::Result<Box<dyn DigestContext>> {
978            Ok(Box::new(FixedDigestContext))
979        }
980
981        fn hmac(
982            &self,
983            _algorithm: DigestAlgorithm,
984            _secret: &[u8],
985        ) -> crate::Result<Box<dyn HmacContext>> {
986            panic!("GCS signed URL should not use HMAC")
987        }
988
989        fn sign(
990            &self,
991            _algorithm: SigningAlgorithm,
992            _pem: &[u8],
993        ) -> crate::Result<Box<dyn Signer>> {
994            Ok(Box::new(FixedSigner))
995        }
996    }
997
998    struct FixedDigestContext;
999
1000    impl DigestContext for FixedDigestContext {
1001        fn update(&mut self, _data: &[u8]) {}
1002
1003        fn finish(&mut self) -> crate::Result<&[u8]> {
1004            Ok(&[0x12, 0x34])
1005        }
1006    }
1007
1008    #[derive(Debug)]
1009    struct UnusedHttpService;
1010
1011    #[async_trait::async_trait]
1012    impl crate::client::HttpService for UnusedHttpService {
1013        async fn call(
1014            &self,
1015            _req: crate::client::HttpRequest,
1016        ) -> std::result::Result<crate::client::HttpResponse, HttpError> {
1017            panic!("SelfSignedJwt should not make HTTP requests")
1018        }
1019    }
1020
1021    #[test]
1022    fn self_signed_jwt_base64url_encodes_raw_signature_bytes() {
1023        let jwt = SelfSignedJwt::new(
1024            "key-id".into(),
1025            "service-account@example.com".into(),
1026            ServiceAccountKey::new(Box::new(FixedSigner)),
1027            DEFAULT_SCOPE.to_string(),
1028        )
1029        .unwrap();
1030        let client = HttpClient::new(UnusedHttpService);
1031        let token = futures_executor::block_on(jwt.fetch_token(&client, &RetryConfig::default()))
1032            .unwrap()
1033            .token;
1034        let signature = token.bearer.rsplit('.').next().unwrap();
1035
1036        assert_eq!(signature, BASE64_URL_SAFE_NO_PAD.encode(SIGNATURE_BYTES));
1037        assert_ne!(
1038            signature,
1039            BASE64_URL_SAFE_NO_PAD.encode(hex_encode(SIGNATURE_BYTES))
1040        );
1041    }
1042
1043    #[test]
1044    fn signed_url_hex_encodes_local_signature_bytes() {
1045        let signing_credential = Arc::new(GcpSigningCredential {
1046            email: "service-account@example.com".into(),
1047            private_key: Some(ServiceAccountKey::new(Box::new(FixedSigner))),
1048        });
1049        let authorizer = GCSAuthorizer::new(Arc::clone(&signing_credential));
1050        let config = GoogleCloudStorageConfig {
1051            base_url: DEFAULT_GCS_BASE_URL.into(),
1052            credentials: Arc::new(StaticCredentialProvider::new(GcpCredential {
1053                bearer: "bearer".into(),
1054            })),
1055            signing_credentials: Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
1056                email: "service-account@example.com".into(),
1057                private_key: None,
1058            })),
1059            crypto: None,
1060            bucket_name: "bucket".into(),
1061            retry_config: RetryConfig::default(),
1062            client_options: ClientOptions::default(),
1063            skip_signature: false,
1064        };
1065        let client =
1066            GoogleCloudStorageClient::new(config, HttpClient::new(UnusedHttpService)).unwrap();
1067        let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
1068
1069        futures_executor::block_on(authorizer.sign_with(
1070            &FixedCryptoProvider,
1071            Method::GET,
1072            &mut url,
1073            &[],
1074            &HeaderMap::new(),
1075            Duration::from_secs(60),
1076            &client,
1077        ))
1078        .unwrap();
1079
1080        let signature = url
1081            .query_pairs()
1082            .find(|(key, _)| key == "X-Goog-Signature")
1083            .unwrap()
1084            .1;
1085        assert_eq!(signature, hex_encode(SIGNATURE_BYTES));
1086    }
1087
1088    #[test]
1089    fn signed_url_with_folds_query_and_headers() {
1090        let signing_credential = Arc::new(GcpSigningCredential {
1091            email: "service-account@example.com".into(),
1092            private_key: Some(ServiceAccountKey::new(Box::new(FixedSigner))),
1093        });
1094        let authorizer = GCSAuthorizer::new(Arc::clone(&signing_credential));
1095        let config = GoogleCloudStorageConfig {
1096            base_url: DEFAULT_GCS_BASE_URL.into(),
1097            credentials: Arc::new(StaticCredentialProvider::new(GcpCredential {
1098                bearer: "bearer".into(),
1099            })),
1100            signing_credentials: Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
1101                email: "service-account@example.com".into(),
1102                private_key: None,
1103            })),
1104            crypto: None,
1105            bucket_name: "bucket".into(),
1106            retry_config: RetryConfig::default(),
1107            client_options: ClientOptions::default(),
1108            skip_signature: false,
1109        };
1110        let client =
1111            GoogleCloudStorageClient::new(config, HttpClient::new(UnusedHttpService)).unwrap();
1112        let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
1113
1114        let mut signed_headers = HeaderMap::new();
1115        signed_headers.insert(
1116            HeaderName::from_static("content-type"),
1117            HeaderValue::from_static("text/plain"),
1118        );
1119
1120        futures_executor::block_on(authorizer.sign_with(
1121            &FixedCryptoProvider,
1122            Method::PUT,
1123            &mut url,
1124            &[
1125                ("partNumber".to_string(), "1".to_string()),
1126                ("uploadId".to_string(), "abc123".to_string()),
1127            ],
1128            &signed_headers,
1129            Duration::from_secs(60),
1130            &client,
1131        ))
1132        .unwrap();
1133
1134        let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
1135        // Caller-provided query parameters are present in the signed URL.
1136        assert_eq!(pairs.get("partNumber").map(String::as_str), Some("1"));
1137        assert_eq!(pairs.get("uploadId").map(String::as_str), Some("abc123"));
1138        // The extra header is reflected in X-Goog-SignedHeaders, sorted alongside `host`.
1139        assert_eq!(
1140            pairs.get("X-Goog-SignedHeaders").map(String::as_str),
1141            Some("content-type;host")
1142        );
1143        assert!(pairs.contains_key("X-Goog-Signature"));
1144    }
1145
1146    #[test]
1147    fn test_canonicalize_headers() {
1148        let mut input_header = HeaderMap::new();
1149        input_header.insert("content-type", "text/plain".parse().unwrap());
1150        input_header.insert("host", "storage.googleapis.com".parse().unwrap());
1151        input_header.insert("x-goog-meta-reviewer", "jane".parse().unwrap());
1152        input_header.append("x-goog-meta-reviewer", "john".parse().unwrap());
1153        assert_eq!(
1154            GCSAuthorizer::canonicalize_headers(&input_header),
1155            (
1156                "content-type:text/plain
1157host:storage.googleapis.com
1158x-goog-meta-reviewer:jane,john"
1159                    .into(),
1160                "content-type;host;x-goog-meta-reviewer".to_string()
1161            )
1162        );
1163    }
1164
1165    #[test]
1166    fn test_canonicalize_query() {
1167        let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
1168        url.query_pairs_mut()
1169            .append_pair("max-keys", "2")
1170            .append_pair("prefix", "object");
1171        assert_eq!(
1172            GCSAuthorizer::canonicalize_query(&url),
1173            "max-keys=2&prefix=object".to_string()
1174        );
1175    }
1176
1177    #[test]
1178    fn trim_header_value_collapses_internal_whitespace() {
1179        // The canonical request collapses runs of whitespace to a single space and trims the
1180        // ends, rather than stripping all whitespace, so a signed `content-type` with parameters
1181        // matches the value the recipient sends.
1182        assert_eq!(trim_header_value("  foo   bar  "), "foo bar");
1183        assert_eq!(
1184            trim_header_value("text/plain;  charset=utf-8"),
1185            "text/plain; charset=utf-8"
1186        );
1187        assert_eq!(trim_header_value("single"), "single");
1188    }
1189}