yup_oauth2/
service_account_impersonator.rs1use 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#[derive(serde::Deserialize, Debug)]
50struct TokenResponse {
51 #[serde(rename = "accessToken")]
53 access_token: String,
54 #[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#[derive(serde::Deserialize, Debug)]
78struct IdTokenResponse {
79 token: String,
80}
81
82impl From<IdTokenResponse> for TokenInfo {
83 fn from(resp: IdTokenResponse) -> TokenInfo {
84 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
98pub struct ServiceAccountImpersonationFlow {
101 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 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 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}