diff --git a/Cargo.lock b/Cargo.lock index b13a403dff..218a92cbea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,7 @@ name = "auth-integration-tests" version = "0.0.0" dependencies = [ "anyhow", + "base64", "bytes", "google-cloud-auth", "google-cloud-bigquery-v2", @@ -99,6 +100,7 @@ dependencies = [ "google-cloud-language-v2", "google-cloud-secretmanager-v1", "httptest", + "reqwest", "scoped-env", "serde_json", "serial_test", diff --git a/src/auth/integration-tests/Cargo.toml b/src/auth/integration-tests/Cargo.toml index 2fddf0150f..36ebc1e4c0 100644 --- a/src/auth/integration-tests/Cargo.toml +++ b/src/auth/integration-tests/Cargo.toml @@ -31,6 +31,8 @@ tempfile.workspace = true serde_json.workspace = true httptest.workspace = true test-case.workspace = true +base64.workspace = true +reqwest.workspace = true # Local dependencies auth = { path = "../../../src/auth", package = "google-cloud-auth" } gax = { path = "../../../src/gax", package = "google-cloud-gax" } diff --git a/src/auth/integration-tests/src/lib.rs b/src/auth/integration-tests/src/lib.rs index 64c39d85eb..6adbc99fe8 100644 --- a/src/auth/integration-tests/src/lib.rs +++ b/src/auth/integration-tests/src/lib.rs @@ -20,10 +20,12 @@ use auth::credentials::{ ProgrammaticBuilder as ExternalAccountProgrammaticBuilder, }, impersonated::Builder as ImpersonatedCredentialsBuilder, + mds::idtoken::Builder as IDTokenMDSBuilder, service_account::Builder as ServiceAccountCredentialsBuilder, subject_token::{Builder as SubjectTokenBuilder, SubjectToken, SubjectTokenProvider}, }; use auth::errors::SubjectTokenProviderError; +use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use bigquery::client::DatasetService; use gax::error::rpc::Code; use httptest::{Expectation, Server, matchers::*, responders::*}; @@ -32,6 +34,7 @@ use language::client::LanguageService; use language::model::Document; use scoped_env::ScopedEnv; use secretmanager::{client::SecretManagerService, model::SecretPayload}; +use serde_json::Value; use std::sync::Arc; pub async fn service_account() -> anyhow::Result<()> { @@ -196,6 +199,44 @@ pub async fn api_key() -> anyhow::Result<()> { Ok(()) } +pub async fn mds_id_token() -> anyhow::Result<()> { + let audience = "https://example.com"; + + // Get the service account email from the metadata server directly + let client = reqwest::Client::new(); + let expected_email = client + .get("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email") + .header("Metadata-Flavor", "Google") + .send() + .await + .expect("failed to get service account email from metadata server") + .text() + .await + .expect("failed to read service account email from metadata server response"); + + // Only works when running on an env that has MDS. + let id_token_creds = IDTokenMDSBuilder::new(audience) + .with_format("full") + .build() + .expect("failed to create id token credentials"); + let token = id_token_creds + .id_token() + .await + .expect("failed to get id token"); + + // Decode the JWT and verify its claims + let parts: Vec<&str> = token.split('.').collect(); + anyhow::ensure!(parts.len() == 3, "ID token is not a valid JWT"); + let payload = URL_SAFE_NO_PAD.decode(parts[1])?; + let claims: Value = serde_json::from_slice(&payload)?; + + assert_eq!(claims["aud"], audience); + assert_eq!(claims["email"], expected_email); + assert_eq!(claims["email_verified"], true); + + Ok(()) +} + pub async fn workload_identity_provider_url_sourced( with_impersonation: bool, ) -> anyhow::Result<()> { diff --git a/src/auth/integration-tests/tests/driver.rs b/src/auth/integration-tests/tests/driver.rs index 2e3198a25d..74e7af7473 100644 --- a/src/auth/integration-tests/tests/driver.rs +++ b/src/auth/integration-tests/tests/driver.rs @@ -40,6 +40,11 @@ mod driver { auth_integration_tests::api_key().await } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn run_mds_id_token() -> anyhow::Result<()> { + auth_integration_tests::mds_id_token().await + } + #[cfg(all(test, feature = "run-byoid-integration-tests"))] #[test_case(false; "without impersonation")] #[test_case(true; "with impersonation")] diff --git a/src/auth/src/credentials.rs b/src/auth/src/credentials.rs index fb3c60d7cb..3afb4f1733 100644 --- a/src/auth/src/credentials.rs +++ b/src/auth/src/credentials.rs @@ -26,8 +26,7 @@ pub mod anonymous; pub mod api_key_credentials; pub mod external_account; pub(crate) mod external_account_sources; -#[allow(dead_code)] -pub(crate) mod idtoken; +pub mod idtoken; pub mod impersonated; pub(crate) mod internal; pub mod mds; diff --git a/src/auth/src/credentials/mds.rs b/src/auth/src/credentials/mds.rs index bd98d76699..fa64e8dbe5 100644 --- a/src/auth/src/credentials/mds.rs +++ b/src/auth/src/credentials/mds.rs @@ -397,8 +397,7 @@ impl TokenProvider for MDSAccessTokenProvider { } } -#[allow(dead_code)] -pub(crate) mod idtoken { +pub mod idtoken { //! Types for fetching ID tokens from the metadata service. use super::{ GCE_METADATA_HOST_ENV_VAR, MDS_DEFAULT_URI, METADATA_FLAVOR, METADATA_FLAVOR_VALUE,