mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2025-12-26 16:27:25 +01:00
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:
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user