1#![forbid(unsafe_code)]
2#![warn(
3 missing_docs,
4 unreachable_pub,
5 unused_crate_dependencies,
6 clippy::pedantic,
7 clippy::nursery,
8 clippy::unwrap_used,
9 clippy::dbg_macro,
10 clippy::todo
11)]
12#![allow(clippy::module_name_repetitions)]
13
14use crate::error::{Result, TokenGenerationError};
40use crate::json_structs::{Claims, GoogleResponse, ServiceAccountInfoJson};
41
42use crate::usage::Usage;
43use jsonwebtoken::{Algorithm, EncodingKey, Header};
44
45pub mod error;
47pub(crate) mod json_structs;
48pub mod usage;
50
51pub type Error = TokenGenerationError;
53
54static GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
55static CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
56
57pub struct AuthConfig {
60 header: Header,
61 iss: String,
62 scope: String,
63 aud: String,
64 private_key: String,
65}
66
67impl AuthConfig {
68 pub fn build(service_account_json_str: &str, usage: &Usage) -> Result<Self> {
90 let account_info: ServiceAccountInfoJson = serde_json::from_str(service_account_json_str)?;
91 Ok(Self {
92 header: Header::new(Algorithm::RS256),
93 iss: account_info.client_email,
94 scope: usage.as_string(),
95 aud: account_info.token_uri,
96 private_key: account_info.private_key,
97 })
98 }
99
100 pub async fn generate_auth_token(&self, lifetime: i64) -> Result<String> {
107 if !(30..=3600).contains(&lifetime) {
108 return Err(Error::InvalidLifetime(lifetime));
109 }
110
111 let claims = Claims::new(
115 self.iss.clone(),
116 self.scope.clone(),
117 self.aud.clone(),
118 lifetime,
119 );
120 let assertion = self.sign(&claims)?;
121
122 let params = format!("grant_type={GRANT_TYPE}&assertion={assertion}");
123 let resp = reqwest::Client::new()
124 .post(&self.aud)
125 .header(reqwest::header::CONTENT_TYPE, CONTENT_TYPE)
126 .body(params)
127 .send()
128 .await?
129 .json::<GoogleResponse>()
130 .await?;
131 match resp {
132 GoogleResponse::ValidResponse { access_token, .. } => Ok(access_token),
133 GoogleResponse::ErrorResponse {
134 error,
135 error_description,
136 ..
137 } => Err(Error::AuthenticationError(error, error_description)),
138 }
139 }
140
141 fn sign(&self, claims: &Claims) -> Result<String> {
142 let key = EncodingKey::from_rsa_pem(self.private_key.as_bytes())?;
143 Ok(jsonwebtoken::encode::<Claims>(&self.header, claims, &key)?)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 #![allow(clippy::unwrap_used)]
150 use super::*;
151 use std::fs;
152
153 #[tokio::test]
154 async fn test_generate_auth_token() {
155 let valid_config = get_valid_config_complete();
156
157 let invalid_config = AuthConfig::build(
160 &fs::read_to_string("tests/test-client-old.json").unwrap(),
161 &Usage::CloudVision,
162 )
163 .unwrap();
164
165 assert!(valid_config.generate_auth_token(3600).await.is_ok());
166 assert!(invalid_config.generate_auth_token(3600).await.is_err());
167 }
168
169 #[tokio::test]
170 async fn test_generate_auth_token_wrong_json() {
171 let config = AuthConfig::build(
172 &fs::read_to_string("tests/invalid-client.json").unwrap(),
173 &Usage::CloudVision,
174 );
175 assert!(config.is_err());
176 }
177
178 #[tokio::test]
179 async fn test_invalid_usage() {
180 let invalid_usage_config = get_valid_config(&Usage::Custom(String::from("invalid usage")));
181 let no_usage_config = get_valid_config(&Usage::Custom(String::new()));
182 assert!(invalid_usage_config
183 .generate_auth_token(3600)
184 .await
185 .is_err());
186 assert!(no_usage_config.generate_auth_token(3600).await.is_err());
187 }
188
189 #[tokio::test]
190 async fn test_lifetime() {
191 let valid_config = get_valid_config_complete();
192 assert!(valid_config.generate_auth_token(3601).await.is_err());
193 assert!(valid_config.generate_auth_token(29).await.is_err());
194 assert!(valid_config.generate_auth_token(30).await.is_ok());
195 assert!(valid_config.generate_auth_token(250).await.is_ok());
196 assert!(valid_config.generate_auth_token(0).await.is_err());
197 assert!(valid_config.generate_auth_token(-10).await.is_err());
198 }
199
200 fn get_valid_config_complete() -> AuthConfig {
201 get_valid_config(&Usage::CloudVision)
202 }
203
204 fn get_valid_config(usage: &Usage) -> AuthConfig {
205 AuthConfig::build(
206 &fs::read_to_string("tests/test-client.json").unwrap(),
207 usage,
208 )
209 .unwrap()
210 }
211}