yup_oauth2/
service_account_impersonator.rs

1//! This module provides an authenticator that uses authorized user secrets
2//! to generate impersonated service account tokens.
3//!
4//! Resources:
5//! - [service account impersonation](https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oauth)
6
7use http::header;
8use http_body_util::BodyExt;
9use serde::Serialize;
10
11use crate::{
12    authorized_user::{AuthorizedUserFlow, AuthorizedUserSecret},
13    client::SendRequest,
14    storage::TokenInfo,
15    Error,
16};
17
18const IAM_CREDENTIALS_ENDPOINT: &str = "https://iamcredentials.googleapis.com";
19
20fn uri(email: &str) -> String {
21    format!(
22        "{}/v1/projects/-/serviceAccounts/{}:generateAccessToken",
23        IAM_CREDENTIALS_ENDPOINT, email
24    )
25}
26
27fn id_uri(email: &str) -> String {
28    format!(
29        "{}/v1/projects/-/serviceAccounts/{}:generateIdToken",
30        IAM_CREDENTIALS_ENDPOINT, email
31    )
32}
33
34#[derive(Serialize)]
35struct Request<'a> {
36    scope: &'a [&'a str],
37    lifetime: &'a str,
38}
39
40#[derive(Serialize)]
41struct IdRequest<'a> {
42    audience: &'a str,
43    #[serde(rename = "includeEmail")]
44    include_email: bool,
45}
46
47// The response to our impersonation request. (Note that the naming is
48// different from `types::AccessToken` even though the data is equivalent.)
49#[derive(serde::Deserialize, Debug)]
50struct TokenResponse {
51    /// The actual token
52    #[serde(rename = "accessToken")]
53    access_token: String,
54    /// The time until the token expires and a new one needs to be requested.
55    /// In RFC3339 format.
56    #[serde(rename = "expireTime")]
57    expires_time: String,
58}
59
60impl From<TokenResponse> for TokenInfo {
61    fn from(resp: TokenResponse) -> TokenInfo {
62        let expires_at = time::OffsetDateTime::parse(
63            &resp.expires_time,
64            &time::format_description::well_known::Rfc3339,
65        )
66        .ok();
67        TokenInfo {
68            access_token: Some(resp.access_token),
69            refresh_token: None,
70            expires_at,
71            id_token: None,
72        }
73    }
74}
75
76// The response to a request for impersonating an ID token.
77#[derive(serde::Deserialize, Debug)]
78struct IdTokenResponse {
79    token: String,
80}
81
82impl From<IdTokenResponse> for TokenInfo {
83    fn from(resp: IdTokenResponse) -> TokenInfo {
84        // The response doesn't include an expiry field, but according to the docs [1]
85        // the tokens are always valid for 1 hour.
86        //
87        // [1] https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oidc
88        let expires_at = time::OffsetDateTime::now_utc() + time::Duration::HOUR;
89        TokenInfo {
90            id_token: Some(resp.token),
91            refresh_token: None,
92            access_token: None,
93            expires_at: Some(expires_at),
94        }
95    }
96}
97
98/// ServiceAccountImpersonationFlow uses user credentials to impersonate a service
99/// account.
100pub struct ServiceAccountImpersonationFlow {
101    // If true, we request an impersonated access token. If false, we request an
102    // impersonated ID token.
103    pub(crate) access_token: bool,
104    pub(crate) inner_flow: AuthorizedUserFlow,
105    pub(crate) service_account_email: String,
106}
107
108impl ServiceAccountImpersonationFlow {
109    pub(crate) fn new(
110        user_secret: AuthorizedUserSecret,
111        service_account_email: &str,
112    ) -> ServiceAccountImpersonationFlow {
113        ServiceAccountImpersonationFlow {
114            access_token: true,
115            inner_flow: AuthorizedUserFlow {
116                secret: user_secret,
117            },
118            service_account_email: service_account_email.to_string(),
119        }
120    }
121
122    pub(crate) async fn token<T>(
123        &self,
124        hyper_client: &impl SendRequest,
125        scopes: &[T],
126    ) -> Result<TokenInfo, Error>
127    where
128        T: AsRef<str>,
129    {
130        let inner_token = self
131            .inner_flow
132            .token(hyper_client, scopes)
133            .await?
134            .access_token
135            .ok_or(Error::MissingAccessToken)?;
136        token_impl(
137            hyper_client,
138            &if self.access_token {
139                uri(&self.service_account_email)
140            } else {
141                id_uri(&self.service_account_email)
142            },
143            self.access_token,
144            &inner_token,
145            scopes,
146        )
147        .await
148    }
149}
150
151fn access_request(
152    uri: &str,
153    inner_token: &str,
154    scopes: &[&str],
155) -> Result<http::Request<String>, Error> {
156    let req_body = Request {
157        scope: scopes,
158        // Max validity is 1h.
159        lifetime: "3600s",
160    };
161    let req_body = serde_json::to_string(&req_body)?;
162    Ok(http::Request::post(uri)
163        .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
164        .header(header::CONTENT_LENGTH, req_body.len())
165        .header(header::AUTHORIZATION, format!("Bearer {}", inner_token))
166        .body(req_body)
167        .unwrap())
168}
169
170fn id_request(
171    uri: &str,
172    inner_token: &str,
173    scopes: &[&str],
174) -> Result<http::Request<String>, Error> {
175    // Only one audience is supported.
176    let audience = scopes.first().unwrap_or(&"");
177    let req_body = IdRequest {
178        audience,
179        include_email: true,
180    };
181    let req_body = serde_json::to_string(&req_body)?;
182    Ok(http::Request::post(uri)
183        .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
184        .header(header::CONTENT_LENGTH, req_body.len())
185        .header(header::AUTHORIZATION, format!("Bearer {}", inner_token))
186        .body(req_body)
187        .unwrap())
188}
189
190pub(crate) async fn token_impl<T>(
191    hyper_client: &impl SendRequest,
192    uri: &str,
193    access_token: bool,
194    inner_token: &str,
195    scopes: &[T],
196) -> Result<TokenInfo, Error>
197where
198    T: AsRef<str>,
199{
200    let scopes: Vec<_> = scopes.iter().map(|s| s.as_ref()).collect();
201    let request = if access_token {
202        access_request(uri, inner_token, &scopes)?
203    } else {
204        id_request(uri, inner_token, &scopes)?
205    };
206
207    log::debug!("requesting impersonated token {:?}", request);
208    let (head, body) = hyper_client.request(request).await?.into_parts();
209    let body = body.collect().await?.to_bytes();
210    log::debug!("received response; head: {:?}, body: {:?}", head, body);
211
212    if access_token {
213        let response: TokenResponse = serde_json::from_slice(&body)?;
214        Ok(response.into())
215    } else {
216        let response: IdTokenResponse = serde_json::from_slice(&body)?;
217        Ok(response.into())
218    }
219}