1#![warn(missing_docs)]
2
3pub mod error;
19
20#[macro_use]
21extern crate log;
22#[macro_use]
23extern crate serde_json;
24
25use eyre::{eyre, Report, Result};
26use frank_jwt::{encode, Algorithm};
27use serde::{Deserialize, Serialize};
28use std::io::Write;
29use std::path::Path;
30use std::time::{SystemTime, UNIX_EPOCH};
31use tempfile::NamedTempFile;
32use url::Url;
33
34#[derive(PartialEq, Debug, Deserialize)]
35#[serde(rename_all = "snake_case")]
36enum ServiceAccountKeyType {
37 ServiceAccount,
38}
39
40#[derive(Debug, Deserialize)]
41struct ServiceAccountKey {
42 r#type: ServiceAccountKeyType,
43 project_id: String,
44 private_key_id: String,
45 private_key: String,
46 client_email: String,
47 client_id: String,
48 auth_uri: Url,
49 token_uri: Url,
50 auth_provider_x509_cert_url: String,
51 client_x509_cert_url: String,
52}
53impl ServiceAccountKey {
54 fn private_key_as_namedtempfile(&self) -> Result<NamedTempFile> {
55 let mut tmpf = NamedTempFile::new()?;
56 tmpf.write_all(self.private_key.as_bytes())?;
57 Ok(tmpf)
58 }
59}
60
61#[derive(Debug, Serialize)]
62struct JWTHeaders;
63
64#[derive(Debug, Serialize)]
65struct JWTPayload {
66 iss: String,
67 scope: Option<String>,
68 aud: String,
69 exp: u64,
70 iat: u64,
71}
72impl JWTPayload {
73 fn new(account: String) -> JWTPayload {
74 let lifetime = 60; let now = SystemTime::now();
77 let secs_since_epoc = now.duration_since(UNIX_EPOCH).unwrap();
78
79 JWTPayload {
80 iss: account,
81 scope: None,
82 aud: "https://oauth2.googleapis.com/token".to_string(),
83 exp: secs_since_epoc.as_secs() + lifetime,
84 iat: secs_since_epoc.as_secs(),
85 }
86 }
87}
88
89#[derive(Debug, Deserialize)]
91pub enum GoogleAccessTokenType {
92 Bearer,
94}
95
96#[derive(Debug, Deserialize)]
98pub struct GoogleAccessToken {
99 pub access_token: String,
101 pub scope: Option<String>,
103 pub token_type: GoogleAccessTokenType,
105 pub expires_in: u64,
107}
108
109#[derive(Debug, Deserialize)]
111pub struct GoogleIDToken {
112 pub id_token: String,
114}
115
116pub struct GoogleServiceAccountAuthenticator {
120 headers: JWTHeaders,
121 payload: JWTPayload,
122 service_account_key: ServiceAccountKey,
123}
124impl GoogleServiceAccountAuthenticator {
125 pub fn new_from_service_account_key_file(
128 keyfile: &Path,
129 ) -> Result<GoogleServiceAccountAuthenticator> {
130 let service_account_key: ServiceAccountKey =
131 serde_json::from_str(&std::fs::read_to_string(keyfile)?)?;
132 let headers = JWTHeaders {};
133 let payload = JWTPayload::new(service_account_key.client_email.clone());
134
135 Ok(GoogleServiceAccountAuthenticator {
136 headers,
137 payload,
138 service_account_key,
139 })
140 }
141
142 fn create_token(&self) -> Result<String> {
143 let private_key = self.service_account_key.private_key_as_namedtempfile()?;
144 let token = encode(
145 json!(self.headers),
146 &private_key.path().to_path_buf(),
147 &json!(self.payload),
148 Algorithm::RS256,
149 )?;
150 private_key.close()?;
151 Ok(token)
152 }
153
154 async fn request(&mut self, scope: String) -> Result<String> {
155 self.payload.scope = Some(scope);
156
157 let token = self.create_token()?;
158
159 let params = [
160 ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
161 ("assertion", &token),
162 ];
163 let client = reqwest::Client::new();
164 let response = client
165 .post("https://oauth2.googleapis.com/token")
166 .form(¶ms)
167 .send()
168 .await?;
169 let status = response.status().as_u16();
170 let text = response.text().await?;
171 trace!("response code = {}, response text = {}", status, text);
172 if status == 200 {
173 return Ok(text);
174 }
175 if (400..500).contains(&status) {
176 let google_jwt_error: crate::error::GoogleJWTError = serde_json::from_str(&text)?;
177 let e: Report = eyre!(google_jwt_error);
178 return Err(e);
179 } else {
180 let e: Report = eyre!(format!(
181 "Unknown HTTP error code from Google authentication service received: {}",
182 status
183 ));
184 return Err(e);
185 }
186 }
187
188 pub async fn request_access_token(&mut self) -> Result<GoogleAccessToken> {
190 let text = self
191 .request("https://www.googleapis.com/auth/prediction".to_string())
192 .await?;
193 let access_token: GoogleAccessToken = serde_json::from_str(&text)?;
194 Ok(access_token)
195 }
196
197 pub async fn request_id_token(&mut self, scope: String) -> Result<GoogleIDToken> {
199 let text = self.request(scope).await?;
200 let id_token: GoogleIDToken = serde_json::from_str(&text)?;
201 Ok(id_token)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use crate::{GoogleServiceAccountAuthenticator, ServiceAccountKeyType};
208
209 const KEYFILE: &str = "test-service-account.json";
210 const PUBFILE: &str = "test-publickey.pem";
211 const SCOPE: &str = "http://test.tld/scope-definition";
212
213 #[test]
214 fn new_service_account_from_key() {
215 let authenticator = GoogleServiceAccountAuthenticator::new_from_service_account_key_file(
216 std::path::Path::new(KEYFILE),
217 )
218 .unwrap();
219
220 assert_eq!(
221 authenticator.service_account_key.r#type,
222 ServiceAccountKeyType::ServiceAccount
223 );
224 assert_eq!(
225 authenticator.service_account_key.client_email,
226 "test-account@test-project-id.iam.gserviceaccount.com".to_string()
227 );
228 }
229
230 #[test]
231 fn create_token() {
232 use frank_jwt::{decode, Algorithm, ValidationOptions};
233
234 let mut authenticator =
235 GoogleServiceAccountAuthenticator::new_from_service_account_key_file(
236 std::path::Path::new(KEYFILE),
237 )
238 .unwrap();
239 authenticator.payload.scope = Some(SCOPE.to_string());
240
241 let token = authenticator.create_token().unwrap();
242
243 let (_header, payload) = decode(
244 &token,
245 &std::path::Path::new(PUBFILE).to_path_buf(),
246 Algorithm::RS256,
247 &ValidationOptions::default(),
248 )
249 .unwrap();
250 println!("{}", _header);
251 println!("{}", payload);
252
253 assert_eq!(
254 payload["iss"],
255 authenticator.service_account_key.client_email
256 );
257 assert_eq!(payload["scope"], SCOPE);
258 }
259}
260
261