google_jwt_auth/
lib.rs

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
14//! Crate google-jwt-auth
15//!
16//! This crate provides the functionality to create authentication tokens for Google requests.
17//!
18//! # Two easy steps to obtain a token:
19//! 1. Create an [`AuthConfig`] instance with [`AuthConfig::build()`]
20//! 2. Generate a token with [`AuthConfig::generate_auth_token()`]
21//!
22//! # Example
23//! ```
24//! use google_jwt_auth::AuthConfig;
25//! use google_jwt_auth::usage::Usage;
26//!
27//! // Step one: Create AuthConfig
28//! let client_json = std::fs::read_to_string("tests/test-client.json").unwrap();
29//! let usage = Usage::CloudVision;
30//!
31//! let config = AuthConfig::build(&client_json, &usage).unwrap();
32//!
33//! // Step two: Generate token
34//! let lifetime = 3600_i64;
35//! let token_response = config.generate_auth_token(lifetime);
36//! ```
37//! After awaiting the `token_response` the result can be obtained.
38
39use crate::error::{Result, TokenGenerationError};
40use crate::json_structs::{Claims, GoogleResponse, ServiceAccountInfoJson};
41
42use crate::usage::Usage;
43use jsonwebtoken::{Algorithm, EncodingKey, Header};
44
45/// This module contains all error types and meanings.
46pub mod error;
47pub(crate) mod json_structs;
48/// This module contains all types of usages and their description.
49pub mod usage;
50
51/// This typing is used to have easy access to the library errors.
52pub 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
57/// This struct contains all necessary information to request an authentication token from Google.
58/// This structure is intended to be reused by the client for several token generation requests.
59pub struct AuthConfig {
60    header: Header,
61    iss: String,
62    scope: String,
63    aud: String,
64    private_key: String,
65}
66
67impl AuthConfig {
68    /// This function generates an auth configuration with the provided information. A config is used to request `auth_tokens`.
69    /// This function generates only tokens with the RS256 encryption like the Google jwt authentication service does.
70    /// # Params
71    /// **`service_account_json_str`: String**<br>
72    /// Each google service account has a json file that can be downloaded in the Google console during the key generation.
73    /// This json file cannot be downloaded twice! A new key must be generated, if the file gets lost!
74    /// The content of this file needs to be provided by this param as string.
75    ///
76    /// **`usage`: String**<br>
77    /// Each google api request requires individual permissions to be executed.
78    /// Beside the service account permission a usage or a scope should be provided.
79    /// See here for more information: [Google Scopes](https://developers.google.com/identity/protocols/oauth2/scopes?hl=en).
80    ///
81    /// **`lifetime`: u16**<br>
82    /// An `auth_token` has a limited lifetime to am maximum of 3600 seconds.
83    /// This value should be between 30 and 3600 Seconds.
84    /// Inputs out of this ranged will not be accepted.
85    /// # Errors
86    /// See [`Error`] for a more detailed answer.
87    /// # Returns
88    /// The above-mentioned jwt as String.
89    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    /// With the provided jwt token, an authentication token (short: `auth_token`) will be requested from Google.
101    /// This `auth_token` will be returned and is used for requesting several google api services.
102    /// # Errors
103    /// See [`Error`] for a more detailed answer.
104    /// # Returns
105    /// The above-mentioned `auth_token` as String.
106    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        // TODO add token buffer with lifetime check to minimize auth_token requests
112        // <--
113
114        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        // The following config depends on a deleted service account key.
158        // It is no longer possible to create tokens with this info.
159        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}