1use crate::client::SendRequest;
15use crate::error::Error;
16use crate::types::TokenInfo;
17
18use std::{io, path::PathBuf};
19
20use base64::Engine as _;
21
22use http::header;
23use http_body_util::BodyExt;
24#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
25use rustls::crypto::aws_lc_rs as crypto_provider;
26#[cfg(feature = "ring")]
27use rustls::crypto::ring as crypto_provider;
28use rustls::pki_types::pem::PemObject;
29use rustls::{self, pki_types::PrivateKeyDer, sign::SigningKey};
30use serde::{Deserialize, Serialize};
31use time::OffsetDateTime;
32use url::form_urlencoded;
33
34const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
35const GOOGLE_RS256_HEAD: &str = r#"{"alg":"RS256","typ":"JWT"}"#;
36
37fn append_base64<T: AsRef<[u8]> + ?Sized>(s: &T, out: &mut String) {
39 base64::engine::general_purpose::URL_SAFE.encode_string(s, out)
40}
41
42#[derive(Serialize, Deserialize, Debug, Clone)]
49pub struct ServiceAccountKey {
50 #[serde(rename = "type")]
51 pub key_type: Option<String>,
53 pub project_id: Option<String>,
55 pub private_key_id: Option<String>,
57 pub private_key: String,
59 pub client_email: String,
61 pub client_id: Option<String>,
63 pub auth_uri: Option<String>,
65 pub token_uri: String,
67 pub auth_provider_x509_cert_url: Option<String>,
69 pub client_x509_cert_url: Option<String>,
71}
72
73#[derive(Serialize, Debug)]
76struct Claims<'a> {
77 iss: &'a str,
78 aud: &'a str,
79 exp: i64,
80 iat: i64,
81 #[serde(rename = "sub")]
82 subject: Option<&'a str>,
83 scope: String,
84}
85
86impl<'a> Claims<'a> {
87 fn new<T>(key: &'a ServiceAccountKey, scopes: &[T], subject: Option<&'a str>) -> Self
88 where
89 T: AsRef<str>,
90 {
91 let iat = OffsetDateTime::now_utc().unix_timestamp();
92 let expiry = iat + 3600 - 5; let scope = crate::helper::join(scopes, " ");
95 Claims {
96 iss: &key.client_email,
97 aud: &key.token_uri,
98 exp: expiry,
99 iat,
100 subject,
101 scope,
102 }
103 }
104}
105
106pub(crate) struct JWTSigner {
108 signer: Box<dyn rustls::sign::Signer>,
109}
110
111impl JWTSigner {
112 fn new(private_key: &str) -> Result<Self, io::Error> {
113 let key = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|_| {
114 io::Error::new(io::ErrorKind::InvalidInput, "Error reading key from PEM")
115 })?;
116 let signing_key = crypto_provider::sign::RsaSigningKey::new(&key)
117 .map_err(|_| io::Error::other("Couldn't initialize signer"))?;
118 let signer = signing_key
119 .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
120 .ok_or_else(|| io::Error::other("Couldn't choose signing scheme"))?;
121 Ok(JWTSigner { signer })
122 }
123
124 fn sign_claims(&self, claims: &Claims) -> Result<String, rustls::Error> {
125 let mut jwt_head = Self::encode_claims(claims);
126 let signature = self.signer.sign(jwt_head.as_bytes())?;
127 jwt_head.push('.');
128 append_base64(&signature, &mut jwt_head);
129 Ok(jwt_head)
130 }
131
132 fn encode_claims(claims: &Claims) -> String {
135 let mut head = String::new();
136 append_base64(GOOGLE_RS256_HEAD, &mut head);
137 head.push('.');
138 append_base64(&serde_json::to_string(&claims).unwrap(), &mut head);
139 head
140 }
141}
142
143pub struct ServiceAccountFlowOpts {
144 pub(crate) key: FlowOptsKey,
145 pub(crate) subject: Option<String>,
146}
147
148pub(crate) enum FlowOptsKey {
150 Path(PathBuf),
152 Key(Box<ServiceAccountKey>),
154}
155
156pub struct ServiceAccountFlow {
158 key: ServiceAccountKey,
159 subject: Option<String>,
160 signer: JWTSigner,
161}
162
163impl ServiceAccountFlow {
164 pub(crate) async fn new(opts: ServiceAccountFlowOpts) -> Result<Self, io::Error> {
165 let key = match opts.key {
166 FlowOptsKey::Path(path) => crate::read_service_account_key(path).await?,
167 FlowOptsKey::Key(key) => *key,
168 };
169
170 let signer = JWTSigner::new(&key.private_key)?;
171 Ok(ServiceAccountFlow {
172 key,
173 subject: opts.subject,
174 signer,
175 })
176 }
177
178 pub(crate) async fn token<T>(
180 &self,
181 hyper_client: &impl SendRequest,
182 scopes: &[T],
183 ) -> Result<TokenInfo, Error>
184 where
185 T: AsRef<str>,
186 {
187 let claims = Claims::new(&self.key, scopes, self.subject.as_deref());
188 let signed = self
189 .signer
190 .sign_claims(&claims)
191 .map_err(|_| Error::LowLevelError(io::Error::other("unable to sign claims")))?;
192 let rqbody = form_urlencoded::Serializer::new(String::new())
193 .extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())])
194 .finish();
195 let request = http::Request::post(&self.key.token_uri)
196 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
197 .body(rqbody)
198 .unwrap();
199 log::debug!("requesting token from service account: {:?}", request);
200 let (head, body) = hyper_client.request(request).await?.into_parts();
201 let body = body.collect().await?.to_bytes();
202 log::debug!("received response; head: {:?}, body: {:?}", head, body);
203 TokenInfo::from_json(&body)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::helper::read_service_account_key;
211
212 const TEST_PRIVATE_KEY_PATH: &str = "examples/Sanguine-69411a0c0eea.json";
214
215 #[cfg(feature = "hyper-rustls")]
217 #[allow(dead_code)]
219 async fn test_service_account_e2e() {
220 let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts {
221 key: FlowOptsKey::Path(TEST_PRIVATE_KEY_PATH.into()),
222 subject: None,
223 })
224 .await
225 .unwrap();
226 let client = crate::client::HttpClient::new(
227 hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
228 .build(
229 hyper_rustls::HttpsConnectorBuilder::new()
230 .with_provider_and_native_roots(crypto_provider::default_provider())
231 .unwrap()
232 .https_only()
233 .enable_http1()
234 .enable_http2()
235 .build(),
236 ),
237 None,
238 );
239 println!(
240 "{:?}",
241 acc.token(&client, &["https://www.googleapis.com/auth/pubsub"])
242 .await
243 );
244 println!(
245 "{:?}",
246 acc.token(
247 &client,
248 &["https://some.scope/likely-to-hand-out-id-tokens"]
249 )
250 .await
251 );
252 }
253
254 #[tokio::test]
255 async fn test_jwt_initialize_claims() {
256 let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
257 .await
258 .unwrap();
259 let scopes = vec!["scope1", "scope2", "scope3"];
260 let claims = Claims::new(&key, &scopes, None);
261
262 assert_eq!(
263 claims.iss,
264 "oauth2-public-test@sanguine-rhythm-105020.iam.gserviceaccount.com".to_string()
265 );
266 assert_eq!(claims.scope, "scope1 scope2 scope3".to_string());
267 assert_eq!(
268 claims.aud,
269 "https://accounts.google.com/o/oauth2/token".to_string()
270 );
271 assert!(claims.exp > 1000000000);
272 assert!(claims.iat < claims.exp);
273 assert_eq!(claims.exp - claims.iat, 3595);
274 }
275
276 #[tokio::test]
277 async fn test_jwt_sign() {
278 let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
279 .await
280 .unwrap();
281 let scopes = vec!["scope1", "scope2", "scope3"];
282 let signer = JWTSigner::new(&key.private_key).unwrap();
283 let claims = Claims::new(&key, &scopes, None);
284 let signature = signer.sign_claims(&claims);
285
286 assert!(signature.is_ok());
287
288 let signature = signature.unwrap();
289 assert_eq!(
290 signature.split('.').next().unwrap(),
291 "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
292 );
293 }
294}