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