systemprompt_security/google/
mod.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
27
28use jsonwebtoken::{Algorithm, EncodingKey, Header};
29use serde::{Deserialize, Serialize};
30
31use crate::credential::cache::clamp_ttl;
32use crate::credential::{CredentialError, ProviderCredential, http};
33
34const ASSERTION_TTL: Duration = Duration::from_secs(3600);
36
37const CLOCK_SKEW: Duration = Duration::from_secs(60);
42
43const SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
44
45pub(crate) const SERVICE_ACCOUNT_TYPE: &str = "service_account";
46
47#[derive(Clone, Deserialize)]
48pub struct ServiceAccountKey {
49 pub client_email: String,
50 pub private_key: String,
51 pub project_id: String,
56 #[serde(default = "default_token_uri")]
57 pub token_uri: String,
58}
59
60impl std::fmt::Debug for ServiceAccountKey {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("ServiceAccountKey")
67 .field("client_email", &self.client_email)
68 .field("project_id", &self.project_id)
69 .field("token_uri", &self.token_uri)
70 .field("private_key", &"<redacted>")
71 .finish()
72 }
73}
74
75fn default_token_uri() -> String {
76 "https://oauth2.googleapis.com/token".to_owned()
77}
78
79impl ServiceAccountKey {
80 pub fn parse(secret: &str) -> Result<Option<Self>, CredentialError> {
81 match ProviderCredential::parse(secret)? {
82 ProviderCredential::GoogleServiceAccount(key) => Ok(Some(*key)),
83 ProviderCredential::ApiKey(_) => Ok(None),
84 }
85 }
86}
87
88#[derive(Debug, Serialize)]
89struct Assertion<'a> {
90 iss: &'a str,
91 scope: &'a str,
92 aud: &'a str,
93 iat: u64,
94 exp: u64,
95}
96
97#[derive(Debug, Deserialize)]
98struct TokenResponse {
99 access_token: String,
100 #[serde(default)]
101 expires_in: Option<u64>,
102}
103
104pub async fn access_token(
105 cache_key: &str,
106 key: &ServiceAccountKey,
107) -> Result<String, CredentialError> {
108 crate::credential::cache::token_for(cache_key, || async {
109 let response = exchange(key).await?;
110 Ok((response.access_token, clamp_ttl(response.expires_in)))
111 })
112 .await
113}
114
115async fn exchange(key: &ServiceAccountKey) -> Result<TokenResponse, CredentialError> {
116 let assertion = sign_assertion(key)?;
117 let form = [
118 (
119 "grant_type",
120 "urn:ietf:params:oauth:grant-type:jwt-bearer".to_owned(),
121 ),
122 ("assertion", assertion),
123 ];
124
125 let body = http::post_form(&key.token_uri, &form).await?;
126 serde_json::from_str(&body).map_err(|e| CredentialError::UnreadableBody(e.to_string()))
127}
128
129fn sign_assertion(key: &ServiceAccountKey) -> Result<String, CredentialError> {
130 let now = SystemTime::now()
131 .duration_since(UNIX_EPOCH)
132 .map_err(|e| CredentialError::Clock(e.to_string()))?
133 .as_secs()
134 .saturating_sub(CLOCK_SKEW.as_secs());
135
136 let claims = Assertion {
137 iss: &key.client_email,
138 scope: SCOPE,
139 aud: &key.token_uri,
140 iat: now,
141 exp: now + ASSERTION_TTL.as_secs(),
142 };
143
144 let encoding = EncodingKey::from_rsa_pem(key.private_key.as_bytes())
145 .map_err(|e| CredentialError::SigningKey(e.to_string()))?;
146
147 jsonwebtoken::encode(&Header::new(Algorithm::RS256), &claims, &encoding)
148 .map_err(|e| CredentialError::Sign(e.to_string()))
149}