google_auth_helpers/
service_account.rs

1use std::time::SystemTime;
2
3use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
4use serde::Deserialize;
5use serde_json::Value;
6
7use crate::trait_definitions::GoogleWorkspaceCredentials;
8
9#[derive(Debug, Clone, Deserialize)]
10pub struct ServiceAccount {
11    pub client_email: String,
12    pub private_key_id: String,
13    pub private_key: String,
14    pub token_uri: String,
15}
16
17#[derive(Debug, Clone)]
18pub struct ServiceAccountGoogleWorkspaceCredentials {
19    pub scopes: &'static [&'static str],
20    pub subject: Option<String>,
21    pub credentials: ServiceAccount,
22}
23
24#[derive(serde::Serialize)]
25struct Claims<'a> {
26    iss: &'a str,
27    scope: &'a str,
28    aud: &'a str,
29    iat: u64,
30    exp: u64,
31    sub: Option<&'a str>,
32}
33
34impl ServiceAccountGoogleWorkspaceCredentials {
35    pub fn new(service_account: ServiceAccount) -> Self {
36        Self {
37            credentials: service_account,
38            scopes: &["https://www.googleapis.com/auth/admin.directory.user"],
39            subject: None,
40        }
41    }
42
43    pub fn with_subject(mut self, subject: &str) -> Self {
44        self.subject = Some(subject.to_string());
45        self
46    }
47    pub async fn token(&self) -> anyhow::Result<Value> {
48        const EXPIRE: u64 = 60 * 60;
49
50        let iat = issued_at();
51        let scope = &self.scopes.join(" ");
52        let claims = Claims {
53            iss: &self.credentials.client_email,
54            scope: scope.as_str(),
55            aud: &self.credentials.token_uri,
56            iat,
57            exp: iat + EXPIRE,
58            sub: self.subject.as_deref(),
59        };
60
61        let header = Header {
62            typ: Some("JWT".into()),
63            alg: Algorithm::RS256,
64            kid: Some(self.credentials.private_key_id.clone().into()),
65            ..Default::default()
66        };
67        let private_key = EncodingKey::from_rsa_pem(self.credentials.private_key.as_bytes())?;
68
69        let client: reqwest::RequestBuilder =
70            reqwest::Client::new().post(&self.credentials.token_uri);
71        let assertion = encode(&header, &claims, &private_key)?;
72        let req = client.json(&serde_json::json!({
73
74            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
75            "assertion": assertion,
76        }));
77        let output = req
78            .send()
79            .await?
80            .error_for_status()?
81            .json::<Value>()
82            .await?;
83        Ok(output)
84    }
85}
86
87#[async_trait::async_trait]
88impl GoogleWorkspaceCredentials for ServiceAccountGoogleWorkspaceCredentials {
89    async fn get_access_token(&self) -> anyhow::Result<String> {
90        let token = self.token().await?;
91        let access_token = token
92            .get("access_token")
93            .ok_or(anyhow::anyhow!("Failed to retrieve access token"))?
94            .as_str()
95            .ok_or(anyhow::anyhow!("Failed to convert access token to string"))?
96            .to_owned();
97        Ok(access_token)
98    }
99}
100
101fn issued_at() -> u64 {
102    SystemTime::UNIX_EPOCH.elapsed().unwrap().as_secs() - 10
103}