blob: 50c333261db5b8d60afcd447ddb0f9c76b953e6c [file] [log] [blame]
use std::time::Duration;
use anyhow::{bail, Result};
use http::header::{ACCEPT, CONTENT_TYPE};
use log::error;
use serde::Deserialize;
use crate::google::credential::{external_account::CredentialSource, ExternalAccount};
use super::{Token, TokenLoader};
/// The maximum impersonated token lifetime allowed, 1 hour.
const MAX_LIFETIME: Duration = Duration::from_secs(3600);
#[derive(Clone, Deserialize, Default)]
#[cfg_attr(test, derive(Debug))]
#[serde(default, rename_all = "camelCase")]
struct ImpersonatedToken {
access_token: String,
expire_time: String,
}
// As documented in https://google.aip.dev/auth/4117
async fn load_security_token(
cred: &ExternalAccount,
oidc_token: &str,
client: &reqwest::Client,
) -> Result<Token> {
// As documented in https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token.
let req = serde_json::json!({
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"audience": &cred.audience,
"scope": "https://www.googleapis.com/auth/cloud-platform",
"subjectToken": oidc_token,
"subjectTokenType": &cred.subject_token_type,
});
let req = serde_json::to_vec(&req)?;
let resp = client
.post(&cred.token_url)
.header(ACCEPT, "application/json")
.header(CONTENT_TYPE, "application/json")
.body(req)
.send()
.await?;
if !resp.status().is_success() {
error!("exchange token got unexpected response: {:?}", resp);
bail!("exchange token failed: {}", resp.text().await?);
}
let token = serde_json::from_slice(&resp.bytes().await?)?;
Ok(token)
}
async fn load_impersonated_token(
cred: &ExternalAccount,
access_token: &str,
scope: &str,
client: &reqwest::Client,
) -> Result<Option<Token>> {
let Some(url) = &cred.service_account_impersonation_url else {
return Ok(None);
};
let lifetime = cred
.service_account_impersonation
.as_ref()
.and_then(|s| s.token_lifetime_seconds)
.unwrap_or(MAX_LIFETIME.as_secs() as usize);
let req = serde_json::json!({
"scope": [scope],
"lifetime": format!("{lifetime}s"),
});
let req = serde_json::to_vec(&req)?;
let resp = client
.post(url)
.header(ACCEPT, "application/json")
.header(CONTENT_TYPE, "application/json")
.bearer_auth(access_token)
.body(req)
.send()
.await?;
if !resp.status().is_success() {
error!("impersonated token got unexpected response: {:?}", resp);
bail!("exchange impersonated token failed: {}", resp.text().await?);
}
let token: ImpersonatedToken = serde_json::from_slice(&resp.bytes().await?)?;
Ok(Some(Token::new(&token.access_token, lifetime, scope)))
}
impl TokenLoader {
/// Exchange token via Google's External Account Credentials.
///
/// Reference: [External Account Credentials (Workload Identity Federation)](https://google.aip.dev/auth/4117)
pub(super) async fn load_via_external_account(&self) -> Result<Option<Token>> {
let Some(cred) = self.credential.as_ref().and_then(|cred| cred.external_account.as_ref()) else {
return Ok(None);
};
let oidc_token =
credential_source::load_oidc_token(&cred.credential_source, &self.client).await?;
let sts = load_security_token(cred, &oidc_token, &self.client).await?;
let token = load_impersonated_token(cred, sts.access_token(), &self.scope, &self.client)
.await?
.unwrap_or(sts);
Ok(Some(token))
}
}
mod credential_source {
use std::io::Read;
use http::{header::HeaderName, HeaderMap, HeaderValue};
use super::*;
use crate::external_account::{FileSourcedCredentials, UrlSourcedCredentials};
pub(super) async fn load_oidc_token(
source: &CredentialSource,
client: &reqwest::Client,
) -> Result<String> {
match source {
CredentialSource::FileSourced(source) => load_file_sourced_oidc_token(source),
CredentialSource::UrlSourced(source) => {
load_url_sourced_oidc_token(source, client).await
}
}
}
async fn load_url_sourced_oidc_token(
source: &UrlSourcedCredentials,
client: &reqwest::Client,
) -> Result<String> {
let headers: HeaderMap = source
.headers
.iter()
.map(|(key, value)| Ok((HeaderName::try_from(key)?, HeaderValue::try_from(value)?)))
.collect::<Result<_>>()?;
let resp = client.get(&source.url).headers(headers).send().await?;
if !resp.status().is_success() {
error!("exchange token got unexpected response: {:?}", resp);
bail!("exchange OIDC token failed: {}", resp.text().await?);
}
let body = resp.bytes().await?;
source.format.parse(&body)
}
fn load_file_sourced_oidc_token(source: &FileSourcedCredentials) -> Result<String> {
let mut file = std::fs::OpenOptions::new().read(true).open(&source.file)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
source.format.parse(&buf)
}
}