Add support for generating impersonated ids.

The previous service account impersonation feature only allowed requesting
impersonated access tokens. This one adds id tokens.
This commit is contained in:
Joe Neeman
2022-11-23 14:43:40 -06:00
parent 78b79cf92c
commit 923a149e99
3 changed files with 113 additions and 24 deletions

View File

@@ -5,18 +5,21 @@ async fn main() {
let svc_email = std::env::args().skip(1).next().unwrap();
let home = std::env::var("HOME").unwrap();
let user_secret =
read_authorized_user_secret(format!("{}/.config/gcloud/application_default_credentials.json", home))
.await
.expect("user secret");
let user_secret = read_authorized_user_secret(format!(
"{}/.config/gcloud/application_default_credentials.json",
home
))
.await
.expect("user secret");
let auth = ServiceAccountImpersonationAuthenticator::builder(user_secret, &svc_email)
.request_id_token()
.build()
.await
.expect("authenticator");
let scopes = &["https://www.googleapis.com/auth/youtube.readonly"];
match auth.token(scopes).await {
match auth.id_token(scopes).await {
Err(e) => println!("error: {:?}", e),
Ok(t) => println!("token: {:?}", t),
}

View File

@@ -81,7 +81,10 @@ where
/// Return a token for the provided scopes, but don't reuse cached tokens. Instead,
/// always fetch a new token from the OAuth server.
pub async fn force_refreshed_token<'a, T>(&'a self, scopes: &'a [T]) -> Result<AccessToken, Error>
pub async fn force_refreshed_token<'a, T>(
&'a self,
scopes: &'a [T],
) -> Result<AccessToken, Error>
where
T: AsRef<str>,
{
@@ -768,6 +771,15 @@ impl<C> AuthenticatorBuilder<C, ServiceAccountImpersonationFlow> {
)
.await
}
/// Configure this authenticator to impersonate an ID token (rather an an access token,
/// as is the default).
///
/// For more on impersonating ID tokens, see [google's docs](https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oidc).
pub fn request_id_token(mut self) -> Self {
self.auth_flow.access_token = false;
self
}
}
/// ## Methods available when building an access token flow Authenticator.

View File

@@ -26,16 +26,28 @@ fn uri(email: &str) -> String {
)
}
fn id_uri(email: &str) -> String {
format!(
"{}/v1/projects/-/serviceAccounts/{}:generateIdToken",
IAM_CREDENTIALS_ENDPOINT, email
)
}
#[derive(Serialize)]
struct Request<'a> {
scope: &'a [&'a str],
lifetime: &'a str,
}
// The impersonation response is in a different format from the other GCP
// responses. Why, Google, why? The response to our impersonation request.
// (Note that the naming is different from `types::AccessToken` even though
// the data is equivalent.)
#[derive(Serialize)]
struct IdRequest<'a> {
audience: &'a str,
#[serde(rename = "includeEmail")]
include_email: bool,
}
// The response to our impersonation request. (Note that the naming is
// different from `types::AccessToken` even though the data is equivalent.)
#[derive(serde::Deserialize, Debug)]
struct TokenResponse {
/// The actual token
@@ -63,9 +75,34 @@ impl From<TokenResponse> for TokenInfo {
}
}
// The response to a request for impersonating an ID token.
#[derive(serde::Deserialize, Debug)]
struct IdTokenResponse {
token: String,
}
impl From<IdTokenResponse> for TokenInfo {
fn from(resp: IdTokenResponse) -> TokenInfo {
// The response doesn't include an expiry field, but according to the docs [1]
// the tokens are always valid for 1 hour.
//
// [1] https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oidc
let expires_at = time::OffsetDateTime::now_utc() + time::Duration::HOUR;
TokenInfo {
id_token: Some(resp.token),
refresh_token: None,
access_token: None,
expires_at: Some(expires_at),
}
}
}
/// ServiceAccountImpersonationFlow uses user credentials to impersonate a service
/// account.
pub struct ServiceAccountImpersonationFlow {
// If true, we request an impersonated access token. If false, we request an
// impersonated ID token.
pub(crate) access_token: bool,
pub(crate) inner_flow: AuthorizedUserFlow,
pub(crate) service_account_email: String,
}
@@ -76,6 +113,7 @@ impl ServiceAccountImpersonationFlow {
service_account_email: &str,
) -> ServiceAccountImpersonationFlow {
ServiceAccountImpersonationFlow {
access_token: true,
inner_flow: AuthorizedUserFlow {
secret: user_secret,
},
@@ -83,6 +121,45 @@ impl ServiceAccountImpersonationFlow {
}
}
fn access_request(
&self,
inner_token: &str,
scopes: &[&str],
) -> Result<hyper::Request<hyper::Body>, Error> {
let req_body = Request {
scope: scopes,
// Max validity is 1h.
lifetime: "3600s",
};
let req_body = serde_json::to_vec(&req_body)?;
Ok(hyper::Request::post(uri(&self.service_account_email))
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.header(header::CONTENT_LENGTH, req_body.len())
.header(header::AUTHORIZATION, format!("Bearer {}", inner_token))
.body(req_body.into())
.unwrap())
}
fn id_request(
&self,
inner_token: &str,
scopes: &[&str],
) -> Result<hyper::Request<hyper::Body>, Error> {
// Only one audience is supported.
let audience = scopes.first().unwrap_or(&"");
let req_body = IdRequest {
audience,
include_email: true,
};
let req_body = serde_json::to_vec(&req_body)?;
Ok(hyper::Request::post(id_uri(&self.service_account_email))
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.header(header::CONTENT_LENGTH, req_body.len())
.header(header::AUTHORIZATION, format!("Bearer {}", inner_token))
.body(req_body.into())
.unwrap())
}
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<S>,
@@ -103,26 +180,23 @@ impl ServiceAccountImpersonationFlow {
.ok_or(Error::MissingAccessToken)?;
let scopes: Vec<_> = scopes.iter().map(|s| s.as_ref()).collect();
let req_body = Request {
scope: &scopes,
// Max validity is 1h.
lifetime: "3600s",
let request = if self.access_token {
self.access_request(&inner_token, &scopes)?
} else {
self.id_request(&inner_token, &scopes)?
};
let req_body = serde_json::to_vec(&req_body)?;
let request = hyper::Request::post(uri(&self.service_account_email))
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.header(header::CONTENT_LENGTH, req_body.len())
.header(header::AUTHORIZATION, format!("Bearer {}", inner_token))
.body(req_body.into())
.unwrap();
log::debug!("requesting impersonated token {:?}", request);
let (head, body) = hyper_client.request(request).await?.into_parts();
let body = hyper::body::to_bytes(body).await?;
log::debug!("received response; head: {:?}, body: {:?}", head, body);
let response: TokenResponse = serde_json::from_slice(&body)?;
Ok(response.into())
if self.access_token {
let response: TokenResponse = serde_json::from_slice(&body)?;
Ok(response.into())
} else {
let response: IdTokenResponse = serde_json::from_slice(&body)?;
Ok(response.into())
}
}
}