yup_oauth2/
external_account.rs1use crate::client::SendRequest;
9use crate::error::Error;
10use crate::types::TokenInfo;
11use http::header;
12use http_body_util::BodyExt;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use thiserror::Error;
16use url::form_urlencoded;
17
18#[derive(Serialize, Deserialize, Debug, Clone)]
23pub struct ExternalAccountSecret {
24 pub audience: String,
26 pub subject_token_type: String,
28 pub service_account_impersonation_url: Option<String>,
30 pub token_url: String,
32 pub credential_source: CredentialSource,
35 #[serde(rename = "type")]
36 pub key_type: String,
38}
39
40#[derive(Serialize, Deserialize, Debug, Clone)]
42#[serde(untagged)]
43pub enum CredentialSource {
44 File {
46 file: String,
48 },
49
50 Url {
52 url: String,
56 headers: Option<HashMap<String, String>>,
58 format: UrlCredentialSourceFormat,
60 },
61 }
63
64#[derive(Serialize, Deserialize, Debug, Clone)]
67#[serde(tag = "type")]
68pub enum UrlCredentialSourceFormat {
69 #[serde(rename = "text")]
71 Text,
72 #[serde(rename = "json")]
74 Json {
75 subject_token_field_name: String,
77 },
78}
79
80#[derive(Debug, Error)]
81pub enum CredentialSourceError {
83 #[error("Failed to parse credential text source: {0}")]
85 CredentialSourceTextInvalid(std::string::FromUtf8Error),
86 #[error("JSON credential source is invalid: {0}")]
88 JsonInvalid(#[source] serde_json::Error),
89 #[error("JSON credential source is missing field {0}")]
91 MissingJsonField(String),
92 #[error("JSON credential source could not convert field {0} to string")]
94 InvalidJsonField(String),
95}
96
97pub struct ExternalAccountFlow {
99 pub(crate) secret: ExternalAccountSecret,
100}
101
102impl ExternalAccountFlow {
103 pub(crate) async fn token<T>(
105 &self,
106 hyper_client: &impl SendRequest,
107 scopes: &[T],
108 ) -> Result<TokenInfo, Error>
109 where
110 T: AsRef<str>,
111 {
112 let subject_token = match &self.secret.credential_source {
113 CredentialSource::File { file } => tokio::fs::read_to_string(file).await?,
114 CredentialSource::Url {
115 url,
116 headers,
117 format,
118 } => {
119 let request = headers
120 .iter()
121 .flatten()
122 .fold(hyper::Request::get(url), |builder, (name, value)| {
123 builder.header(name, value)
124 })
125 .body(String::new())
126 .unwrap();
127
128 log::debug!("requesting credential from url: {:?}", request);
129 let (head, body) = hyper_client.request(request).await?.into_parts();
130 let body = body.collect().await?.to_bytes();
131 log::debug!("received response; head: {:?}, body: {:?}", head, body);
132
133 match format {
134 UrlCredentialSourceFormat::Text => {
135 String::from_utf8(body.to_vec()).map_err(|e| {
136 Error::CredentialSourceError(
137 CredentialSourceError::CredentialSourceTextInvalid(e),
138 )
139 })?
140 }
141 UrlCredentialSourceFormat::Json {
142 subject_token_field_name,
143 } => serde_json::from_slice::<HashMap<String, serde_json::Value>>(&body)
144 .map_err(|e| {
145 Error::CredentialSourceError(CredentialSourceError::JsonInvalid(e))
146 })?
147 .remove(subject_token_field_name)
148 .ok_or_else(|| {
149 Error::CredentialSourceError(CredentialSourceError::MissingJsonField(
150 subject_token_field_name.to_owned(),
151 ))
152 })?
153 .as_str()
154 .ok_or_else(|| {
155 Error::CredentialSourceError(CredentialSourceError::InvalidJsonField(
156 subject_token_field_name.to_owned(),
157 ))
158 })?
159 .to_string(),
160 }
161 }
162 };
163
164 let req = form_urlencoded::Serializer::new(String::new())
165 .extend_pairs(&[
166 ("audience", self.secret.audience.as_str()),
167 (
168 "grant_type",
169 "urn:ietf:params:oauth:grant-type:token-exchange",
170 ),
171 (
172 "requested_token_type",
173 "urn:ietf:params:oauth:token-type:access_token",
174 ),
175 (
176 "subject_token_type",
177 self.secret.subject_token_type.as_str(),
178 ),
179 ("subject_token", subject_token.as_str()),
180 (
181 "scope",
182 if self.secret.service_account_impersonation_url.is_some() {
183 "https://www.googleapis.com/auth/cloud-platform".to_owned()
184 } else {
185 crate::helper::join(scopes, " ")
186 }
187 .as_str(),
188 ),
189 ])
190 .finish();
191
192 let request = http::Request::post(&self.secret.token_url)
193 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
194 .body(req)
195 .unwrap();
196
197 log::debug!("requesting token from external account: {:?}", request);
198 let (head, body) = hyper_client.request(request).await?.into_parts();
199 let body = body.collect().await?.to_bytes();
200 log::debug!("received response; head: {:?}, body: {:?}", head, body);
201
202 let token_info = TokenInfo::from_json(&body)?;
203
204 if let Some(service_account_impersonation_url) =
205 &self.secret.service_account_impersonation_url
206 {
207 crate::service_account_impersonator::token_impl(
208 hyper_client,
209 service_account_impersonation_url,
210 true,
211 &token_info.access_token.ok_or(Error::MissingAccessToken)?,
212 scopes,
213 )
214 .await
215 } else {
216 Ok(token_info)
217 }
218 }
219}