gcp_sa/
lib.rs

1#![warn(missing_docs)]
2
3//! Google Cloud Platform Service Account authentication abstraction for Rust
4//!
5//! This crate abstracts OAuth2 [workflow] required when exchanging a GCP Service Account JSON formatted
6//! key to either a ID token or an access token.
7//!
8//! [workflow]: https://developers.google.com/identity/protocols/oauth2/service-account
9//!
10//! # Examples
11//!
12//! ```
13//! let authenticator = GoogleServiceAccountAuthenticator::new_from_service_account_key_file(std::path::Path("key.json".to_string())).unwrap();
14//! let id_token = authenticator.request_id_token("http://some.url.tld/scope-definition").await.unwrap();
15//! println!("Authorization: Bearer {}", id_token);
16//! ```
17
18pub 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; // in seconds
75
76        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/// Variants of different Google Access Token types
90#[derive(Debug, Deserialize)]
91pub enum GoogleAccessTokenType {
92    /// Google Access Token type for "Bearer" token. Currently the only one supported by this crate.
93    Bearer,
94}
95
96/// Representation of Google Access Token JSON
97#[derive(Debug, Deserialize)]
98pub struct GoogleAccessToken {
99    /// The base64 encoded String containing the token
100    pub access_token: String,
101    /// The OAuth scope that the token carries
102    pub scope: Option<String>,
103    /// Type of the token.
104    pub token_type: GoogleAccessTokenType,
105    /// Expiration date of the token
106    pub expires_in: u64,
107}
108
109/// Representation of Google ID Token JSON
110#[derive(Debug, Deserialize)]
111pub struct GoogleIDToken {
112    /// The base64 encoded String containing JWT token
113    pub id_token: String,
114}
115
116/// Authenticator service that ingest a Service Account JSON key file and
117/// communicates with Google's authentication API to exchange it into a
118/// access token or an id token.
119pub struct GoogleServiceAccountAuthenticator {
120    headers: JWTHeaders,
121    payload: JWTPayload,
122    service_account_key: ServiceAccountKey,
123}
124impl GoogleServiceAccountAuthenticator {
125    /// Function that builds new authenticator struct that later can be used to communicate with
126    /// Google's authentication API.
127    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(&params)
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    /// Request Access Token from Google's authentication API
189    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    /// Request ID Token (JWT) from Google's authentication API
198    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// eof