1use crate::error::Error;
15use crate::types::TokenInfo;
16
17use std::io;
18
19use actix_web::client as awc;
20use rustls::{
21 self,
22 internal::pemfile,
23 sign::{self, SigningKey},
24 PrivateKey,
25};
26use serde::{Deserialize, Serialize};
27use url::form_urlencoded;
28
29const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
30const GOOGLE_RS256_HEAD: &str = r#"{"alg":"RS256","typ":"JWT"}"#;
31
32fn append_base64<T: AsRef<[u8]> + ?Sized>(
34 s: &T,
35 out: &mut String,
36) {
37 base64::encode_config_buf(s, base64::URL_SAFE, out)
38}
39
40fn decode_rsa_key(pem_pkcs8: &str) -> Result<PrivateKey, io::Error> {
42 let private_keys = pemfile::pkcs8_private_keys(&mut pem_pkcs8.as_bytes());
43
44 match private_keys {
45 Ok(mut keys) if !keys.is_empty() => {
46 keys.truncate(1);
47 Ok(keys.remove(0))
48 }
49 Ok(_) => Err(io::Error::new(
50 io::ErrorKind::InvalidInput,
51 "Not enough private keys in PEM",
52 )),
53 Err(_) => Err(io::Error::new(
54 io::ErrorKind::InvalidInput,
55 "Error reading key from PEM",
56 )),
57 }
58}
59
60#[derive(Serialize, Deserialize, Debug, Clone)]
66pub struct ServiceAccountKey {
67 #[serde(rename = "type")]
68 pub key_type: Option<String>,
70 pub project_id: Option<String>,
72 pub private_key_id: Option<String>,
74 pub private_key: String,
76 pub client_email: String,
78 pub client_id: Option<String>,
80 pub auth_uri: Option<String>,
82 pub token_uri: String,
84 pub auth_provider_x509_cert_url: Option<String>,
86 pub client_x509_cert_url: Option<String>,
88}
89
90#[derive(Serialize, Debug)]
93struct Claims<'a> {
94 iss: &'a str,
95 aud: &'a str,
96 exp: i64,
97 iat: i64,
98 #[serde(rename = "sub")]
99 subject: Option<&'a str>,
100 scope: String,
101}
102
103impl<'a> Claims<'a> {
104 fn new<T>(
105 key: &'a ServiceAccountKey,
106 scopes: &[T],
107 subject: Option<&'a str>,
108 ) -> Self
109 where
110 T: AsRef<str>,
111 {
112 let iat = chrono::Utc::now().timestamp();
113 let expiry = iat + 3600 - 5; let scope = crate::helper::join(scopes, " ");
116 Claims {
117 iss: &key.client_email,
118 aud: &key.token_uri,
119 exp: expiry,
120 iat,
121 subject,
122 scope,
123 }
124 }
125}
126
127pub(crate) struct JWTSigner {
129 signer: Box<dyn rustls::sign::Signer>,
130}
131
132impl JWTSigner {
133 fn new(private_key: &str) -> Result<Self, io::Error> {
134 let key = decode_rsa_key(private_key)?;
135 let signing_key = sign::RSASigningKey::new(&key)
136 .map_err(|_| io::Error::new(io::ErrorKind::Other, "Couldn't initialize signer"))?;
137 let signer = signing_key
138 .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
139 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Couldn't choose signing scheme"))?;
140 Ok(JWTSigner { signer })
141 }
142
143 fn sign_claims(
144 &self,
145 claims: &Claims,
146 ) -> Result<String, rustls::TLSError> {
147 let mut jwt_head = Self::encode_claims(claims);
148 let signature = self.signer.sign(jwt_head.as_bytes())?;
149 jwt_head.push_str(".");
150 append_base64(&signature, &mut jwt_head);
151 Ok(jwt_head)
152 }
153
154 fn encode_claims(claims: &Claims) -> String {
157 let mut head = String::new();
158 append_base64(GOOGLE_RS256_HEAD, &mut head);
159 head.push_str(".");
160 append_base64(&serde_json::to_string(&claims).unwrap(), &mut head);
161 head
162 }
163}
164
165pub struct ServiceAccountFlowOpts {
166 pub(crate) key: ServiceAccountKey,
167 pub(crate) subject: Option<String>,
168}
169
170pub struct ServiceAccountFlow {
172 key: ServiceAccountKey,
173 subject: Option<String>,
174 signer: JWTSigner,
175}
176
177impl ServiceAccountFlow {
178 pub(crate) fn new(opts: ServiceAccountFlowOpts) -> Result<Self, io::Error> {
179 let signer = JWTSigner::new(&opts.key.private_key)?;
180 Ok(ServiceAccountFlow {
181 key: opts.key,
182 subject: opts.subject,
183 signer,
184 })
185 }
186
187 pub(crate) async fn token<T>(
189 &self,
190 client: &awc::Client,
191 scopes: &[T],
192 ) -> Result<TokenInfo, Error>
193 where
194 T: AsRef<str>,
195 {
196 let claims = Claims::new(&self.key, scopes, self.subject.as_ref().map(|x| x.as_str()));
197 let signed = self.signer.sign_claims(&claims).map_err(|_| {
198 Error::LowLevelError(io::Error::new(
199 io::ErrorKind::Other,
200 "unable to sign claims",
201 ))
202 })?;
203 let rqbody = form_urlencoded::Serializer::new(String::new())
204 .extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())])
205 .finish();
206 let mut resp = client
207 .post(&self.key.token_uri)
208 .header("Content-Type", "application/x-www-form-urlencoded")
209 .send_body(rqbody)
210 .await?;
211 let body = resp.body().await?;
212 log::debug!("received response; body: {:?}", body.as_ref());
213 TokenInfo::from_json(body.as_ref())
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::helper::read_service_account_key;
221 use hyper_rustls::HttpsConnector;
222
223 const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json";
225
226 #[allow(dead_code)]
229 async fn test_service_account_e2e() {
230 let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
231 .await
232 .unwrap();
233 let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { key, subject: None }).unwrap();
234 let https = HttpsConnector::new();
235 let client = hyper::Client::builder()
236 .keep_alive(false)
237 .build::<_, hyper::Body>(https);
238 println!(
239 "{:?}",
240 acc
241 .token(&client, &["https://www.googleapis.com/auth/pubsub"])
242 .await
243 );
244 }
245
246 #[tokio::test]
247 async fn test_jwt_initialize_claims() {
248 let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
249 .await
250 .unwrap();
251 let scopes = vec!["scope1", "scope2", "scope3"];
252 let claims = Claims::new(&key, &scopes, None);
253
254 assert_eq!(
255 claims.iss,
256 "oauth2-public-test@sanguine-rhythm-105020.iam.gserviceaccount.com".to_string()
257 );
258 assert_eq!(claims.scope, "scope1 scope2 scope3".to_string());
259 assert_eq!(
260 claims.aud,
261 "https://accounts.google.com/o/oauth2/token".to_string()
262 );
263 assert!(claims.exp > 1000000000);
264 assert!(claims.iat < claims.exp);
265 assert_eq!(claims.exp - claims.iat, 3595);
266 }
267
268 #[tokio::test]
269 async fn test_jwt_sign() {
270 let key = read_service_account_key(TEST_PRIVATE_KEY_PATH)
271 .await
272 .unwrap();
273 let scopes = vec!["scope1", "scope2", "scope3"];
274 let signer = JWTSigner::new(&key.private_key).unwrap();
275 let claims = Claims::new(&key, &scopes, None);
276 let signature = signer.sign_claims(&claims);
277
278 assert!(signature.is_ok());
279
280 let signature = signature.unwrap();
281 assert_eq!(
282 signature.split(".").nth(0).unwrap(),
283 "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
284 );
285 }
286}