1use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::Duration;
11
12use anyhow::anyhow;
13use reqwest::Client as HttpClient;
14use tokio::sync::RwLock;
15use tracing::{debug, info, warn};
16
17use crate::core::config::OAuthConfig;
18use crate::core::error::{GrrError, Result};
19
20mod device;
21mod oauth;
22mod server;
23mod store;
24mod token;
25
26pub use device::DeviceAuthChallenge;
27pub use token::TokenStorage;
28
29use store::TokenStore;
30
31const GOOGLE_TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
32
33pub(crate) const REDIRECT_URI: &str = "http://localhost:3434/oauth/callback";
35
36pub const SCOPES: &[&str] = &[
40 "https://www.googleapis.com/auth/gmail.readonly",
42 "https://www.googleapis.com/auth/gmail.compose",
43 "https://www.googleapis.com/auth/gmail.modify",
44 "https://www.googleapis.com/auth/gmail.labels",
45 "https://mail.google.com/",
46 "https://www.googleapis.com/auth/calendar",
48 "https://www.googleapis.com/auth/drive",
50 "https://www.googleapis.com/auth/contacts",
52 "https://www.googleapis.com/auth/contacts.other.readonly",
53 "https://www.googleapis.com/auth/chat.messages",
55 "https://www.googleapis.com/auth/chat.spaces",
56 "https://www.googleapis.com/auth/chat.delete",
57 "https://www.googleapis.com/auth/chat.memberships",
58 "https://www.googleapis.com/auth/chat.messages.reactions",
59 "https://www.googleapis.com/auth/forms.body",
61 "https://www.googleapis.com/auth/forms.responses.readonly",
62];
63
64pub(crate) fn scopes_joined() -> String {
65 SCOPES.join(" ")
66}
67
68#[derive(Clone)]
70pub struct GoogleAuth {
71 config: OAuthConfig,
72 http_client: HttpClient,
73 token_storage: Arc<RwLock<Option<TokenStorage>>>,
74 store: TokenStore,
75 token_endpoint: String,
76}
77
78impl GoogleAuth {
79 pub async fn new(config: OAuthConfig) -> Result<Self> {
81 Self::with_store(config, TokenStore::auto().await?).await
82 }
83
84 pub async fn new_for(config: OAuthConfig, account: impl Into<String>) -> Result<Self> {
90 let account = store::validate_account_name(account.into().as_str())?;
91 let store = TokenStore::auto_named(&account).await?;
92 Self::with_store(config, store).await
93 }
94
95 async fn with_store(config: OAuthConfig, store: TokenStore) -> Result<Self> {
96 let http_client = HttpClient::builder()
97 .timeout(Duration::from_secs(30))
98 .build()
99 .map_err(|e| GrrError::Config(format!("Failed to create HTTP client: {}", e)))?;
100 Ok(Self {
107 config,
108 http_client,
109 token_storage: Arc::new(RwLock::new(None)),
110 store,
111 token_endpoint: GOOGLE_TOKEN_URL.to_string(),
112 })
113 }
114
115 pub async fn with_account_store(&self, account: impl Into<String>) -> Result<Self> {
122 Self::new_for(self.config.clone(), account).await
123 }
124
125 pub async fn with_default_account_store(&self) -> Result<Self> {
132 Self::with_store(self.config.clone(), TokenStore::default_named().await?).await
133 }
134
135 pub fn token_backend(&self) -> &'static str {
137 self.store.backend()
138 }
139
140 pub fn http(&self) -> &HttpClient {
148 &self.http_client
149 }
150
151 #[doc(hidden)]
156 pub async fn with_token(config: OAuthConfig, storage: TokenStorage) -> Result<Self> {
157 let this = Self::with_store(config, TokenStore::Memory).await?;
158 *this.token_storage.write().await = Some(storage);
159 Ok(this)
160 }
161
162 #[doc(hidden)]
164 pub fn with_token_endpoint(mut self, url: impl Into<String>) -> Self {
165 self.token_endpoint = url.into();
166 self
167 }
168
169 #[doc(hidden)]
173 pub fn with_token_path(mut self, path: PathBuf) -> Self {
174 self.store = TokenStore::file(path);
175 self
176 }
177
178 pub async fn get_access_token(&self) -> Result<String> {
180 let mut storage_guard = self.token_storage.write().await;
181
182 if storage_guard.is_none() {
188 *storage_guard = self.store.load().await?;
189 }
190
191 if let Some(storage) = storage_guard.as_ref() {
192 if !storage.is_expired() {
193 debug!(
194 "Using cached access token ({}s remaining)",
195 storage.remaining_secs()
196 );
197 return Ok(storage.access_token.clone());
198 }
199
200 let Some(refresh_token) = storage.refresh_token.clone() else {
204 return Err(GrrError::Auth(
205 anyhow!(
206 "stored token is expired and has no refresh token; rerun `grr auth login`"
207 )
208 .into(),
209 ));
210 };
211
212 info!("Refreshing expired access token");
213 match self.refresh_token(&refresh_token).await {
214 Ok(new_storage) => {
215 *storage_guard = Some(new_storage.clone());
216 self.save_token(&new_storage).await?;
217 return Ok(new_storage.access_token);
218 }
219 Err(e) => {
220 warn!("Token refresh failed: {}", e);
221 return Err(GrrError::Auth(
224 anyhow!("{e}; rerun `grr auth login`").into(),
225 ));
226 }
227 }
228 }
229
230 info!("No valid token, starting OAuth flow");
232 let storage = self.run_oauth_flow().await?;
233 *storage_guard = Some(storage.clone());
234 self.save_token(&storage).await?;
235 Ok(storage.access_token)
236 }
237
238 async fn token_request(
247 &self,
248 mut form: Vec<(&str, String)>,
249 ) -> Result<(reqwest::StatusCode, serde_json::Value)> {
250 if let Some(secret) = self
251 .config
252 .client_secret
253 .as_deref()
254 .filter(|s| !s.is_empty())
255 {
256 form.push(("client_secret", secret.to_string()));
257 }
258
259 let response = self
260 .http_client
261 .post(self.token_endpoint.as_str())
262 .form(&form)
263 .send()
264 .await
265 .map_err(GrrError::Http)?;
266
267 let status = response.status();
268 let body_text = response.text().await.map_err(GrrError::Http)?;
269 let body: serde_json::Value = serde_json::from_str(&body_text).map_err(|e| {
270 GrrError::Auth(
271 anyhow!("token endpoint returned {status}: unparseable body ({e})").into(),
272 )
273 })?;
274 Ok((status, body))
275 }
276
277 async fn refresh_token(&self, refresh_token: &str) -> Result<TokenStorage> {
281 let (status, token_data) = self
282 .token_request(vec![
283 ("client_id", self.config.client_id.clone()),
284 ("refresh_token", refresh_token.to_string()),
285 ("grant_type", "refresh_token".to_string()),
286 ])
287 .await?;
288
289 if !status.is_success() {
290 return Err(GrrError::Auth(
293 anyhow!(
294 "token endpoint returned {}: {}",
295 status,
296 crate::core::error::json_error_detail(&token_data)
297 )
298 .into(),
299 ));
300 }
301
302 let mut storage = self::device::token_storage_from_response(&token_data, SCOPES)?;
303
304 if storage.refresh_token.is_none() {
308 storage.refresh_token = Some(refresh_token.to_string());
309 }
310
311 Ok(storage)
312 }
313
314 pub async fn revoke(&self) -> Result<()> {
316 let mut storage_guard = self.token_storage.write().await;
317 *storage_guard = None;
318 self.store.delete().await?;
319 info!("Token revoked and removed");
320 Ok(())
321 }
322}
323
324pub struct AuthConfigBuilder {
325 config: OAuthConfig,
326}
327
328impl AuthConfigBuilder {
329 pub fn new() -> Self {
330 Self {
331 config: OAuthConfig::default(),
332 }
333 }
334
335 pub fn client_id(mut self, id: impl Into<String>) -> Self {
336 self.config.client_id = id.into();
337 self
338 }
339
340 pub fn client_secret(mut self, secret: Option<String>) -> Self {
341 self.config.client_secret = secret.filter(|s| !s.is_empty());
343 self
344 }
345
346 pub async fn build(self) -> Result<GoogleAuth> {
347 GoogleAuth::new(self.config).await
348 }
349}
350
351impl Default for AuthConfigBuilder {
352 fn default() -> Self {
353 Self::new()
354 }
355}