1use std::path::PathBuf;
9use std::sync::Arc;
10use std::sync::LazyLock;
11use std::time::Duration;
12
13use anyhow::anyhow;
14use reqwest::Client as HttpClient;
15use tokio::sync::RwLock;
16use tracing::{debug, info, warn};
17
18use crate::core::config::OAuthConfig;
19use crate::core::error::{GrrError, Result};
20
21mod device;
22mod oauth;
23mod server;
24mod store;
25mod token;
26
27pub use device::DeviceAuthChallenge;
28pub use token::TokenStorage;
29
30use store::TokenStore;
31
32const GOOGLE_TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
33
34pub(crate) const REDIRECT_URI: &str = "http://localhost:3434/oauth/callback";
36
37pub fn scopes() -> &'static [String] {
44 static SCOPES: LazyLock<Vec<String>> = LazyLock::new(|| {
45 let mut set: Vec<String> = CURATED_SCOPES.iter().map(|s| (*s).to_owned()).collect();
46 set.extend(crate::discovery::all_scopes());
47 set.sort_unstable();
48 set.dedup();
49 set
50 });
51 &SCOPES
52}
53
54pub(crate) fn scopes_joined() -> String {
56 scopes().join(" ")
57}
58
59const CURATED_SCOPES: &[&str] = &[
64 "https://www.googleapis.com/auth/gmail.readonly",
66 "https://www.googleapis.com/auth/gmail.compose",
67 "https://www.googleapis.com/auth/gmail.modify",
68 "https://www.googleapis.com/auth/gmail.labels",
69 "https://mail.google.com/",
70 "https://www.googleapis.com/auth/calendar",
72 "https://www.googleapis.com/auth/drive",
74 "https://www.googleapis.com/auth/contacts",
76 "https://www.googleapis.com/auth/contacts.other.readonly",
77 "https://www.googleapis.com/auth/chat.messages",
79 "https://www.googleapis.com/auth/chat.spaces",
80 "https://www.googleapis.com/auth/chat.delete",
81 "https://www.googleapis.com/auth/chat.memberships",
82 "https://www.googleapis.com/auth/chat.messages.reactions",
83 "https://www.googleapis.com/auth/forms.body",
85 "https://www.googleapis.com/auth/forms.responses.readonly",
86 "https://www.googleapis.com/auth/webmasters.readonly",
88];
89
90#[derive(Clone)]
92pub struct GoogleAuth {
93 config: OAuthConfig,
94 http_client: HttpClient,
95 token_storage: Arc<RwLock<Option<TokenStorage>>>,
96 store: TokenStore,
97 token_endpoint: String,
98}
99
100impl GoogleAuth {
101 pub async fn new(config: OAuthConfig) -> Result<Self> {
103 Self::with_store(config, TokenStore::auto().await?).await
104 }
105
106 pub async fn new_for(config: OAuthConfig, account: impl Into<String>) -> Result<Self> {
112 let account = store::validate_account_name(account.into().as_str())?;
113 let store = TokenStore::auto_named(&account).await?;
114 Self::with_store(config, store).await
115 }
116
117 async fn with_store(config: OAuthConfig, store: TokenStore) -> Result<Self> {
118 let http_client = HttpClient::builder()
119 .timeout(Duration::from_secs(30))
120 .build()
121 .map_err(|e| GrrError::Config(format!("Failed to create HTTP client: {}", e)))?;
122 Ok(Self {
129 config,
130 http_client,
131 token_storage: Arc::new(RwLock::new(None)),
132 store,
133 token_endpoint: GOOGLE_TOKEN_URL.to_string(),
134 })
135 }
136
137 pub async fn with_account_store(&self, account: impl Into<String>) -> Result<Self> {
144 Self::new_for(self.config.clone(), account).await
145 }
146
147 pub async fn with_default_account_store(&self) -> Result<Self> {
154 Self::with_store(self.config.clone(), TokenStore::default_named().await?).await
155 }
156
157 pub fn token_backend(&self) -> &'static str {
159 self.store.backend()
160 }
161
162 pub fn http(&self) -> &HttpClient {
170 &self.http_client
171 }
172
173 #[doc(hidden)]
178 pub async fn with_token(config: OAuthConfig, storage: TokenStorage) -> Result<Self> {
179 let this = Self::with_store(config, TokenStore::Memory).await?;
180 *this.token_storage.write().await = Some(storage);
181 Ok(this)
182 }
183
184 #[doc(hidden)]
186 pub fn with_token_endpoint(mut self, url: impl Into<String>) -> Self {
187 self.token_endpoint = url.into();
188 self
189 }
190
191 #[doc(hidden)]
195 pub fn with_token_path(mut self, path: PathBuf) -> Self {
196 self.store = TokenStore::file(path);
197 self
198 }
199
200 pub async fn get_access_token(&self) -> Result<String> {
202 let mut storage_guard = self.token_storage.write().await;
203
204 if storage_guard.is_none() {
210 *storage_guard = self.store.load().await?;
211 }
212
213 if let Some(storage) = storage_guard.as_ref() {
214 if !storage.is_expired() {
215 debug!(
216 "Using cached access token ({}s remaining)",
217 storage.remaining_secs()
218 );
219 return Ok(storage.access_token.clone());
220 }
221
222 let Some(refresh_token) = storage.refresh_token.clone() else {
226 return Err(GrrError::Auth(
227 anyhow!(
228 "stored token is expired and has no refresh token; rerun `grr auth login`"
229 )
230 .into(),
231 ));
232 };
233
234 info!("Refreshing expired access token");
235 match self.refresh_token(&refresh_token).await {
236 Ok(new_storage) => {
237 *storage_guard = Some(new_storage.clone());
238 self.save_token(&new_storage).await?;
239 return Ok(new_storage.access_token);
240 }
241 Err(e) => {
242 warn!("Token refresh failed: {}", e);
243 return Err(GrrError::Auth(
246 anyhow!("{e}; rerun `grr auth login`").into(),
247 ));
248 }
249 }
250 }
251
252 info!("No valid token, starting OAuth flow");
254 let storage = self.run_oauth_flow().await?;
255 *storage_guard = Some(storage.clone());
256 self.save_token(&storage).await?;
257 Ok(storage.access_token)
258 }
259
260 async fn token_request(
269 &self,
270 mut form: Vec<(&str, String)>,
271 ) -> Result<(reqwest::StatusCode, serde_json::Value)> {
272 if let Some(secret) = self
273 .config
274 .client_secret
275 .as_deref()
276 .filter(|s| !s.is_empty())
277 {
278 form.push(("client_secret", secret.to_string()));
279 }
280
281 let response = self
282 .http_client
283 .post(self.token_endpoint.as_str())
284 .form(&form)
285 .send()
286 .await
287 .map_err(GrrError::Http)?;
288
289 let status = response.status();
290 let body_text = response.text().await.map_err(GrrError::Http)?;
291 let body: serde_json::Value = serde_json::from_str(&body_text).map_err(|e| {
292 GrrError::Auth(
293 anyhow!("token endpoint returned {status}: unparseable body ({e})").into(),
294 )
295 })?;
296 Ok((status, body))
297 }
298
299 async fn refresh_token(&self, refresh_token: &str) -> Result<TokenStorage> {
303 let (status, token_data) = self
304 .token_request(vec![
305 ("client_id", self.config.client_id.clone()),
306 ("refresh_token", refresh_token.to_string()),
307 ("grant_type", "refresh_token".to_string()),
308 ])
309 .await?;
310
311 if !status.is_success() {
312 return Err(GrrError::Auth(
315 anyhow!(
316 "token endpoint returned {}: {}",
317 status,
318 crate::core::error::json_error_detail(&token_data)
319 )
320 .into(),
321 ));
322 }
323
324 let mut storage = self::device::token_storage_from_response(&token_data, scopes())?;
325
326 if storage.refresh_token.is_none() {
330 storage.refresh_token = Some(refresh_token.to_string());
331 }
332
333 Ok(storage)
334 }
335
336 pub async fn revoke(&self) -> Result<()> {
338 let mut storage_guard = self.token_storage.write().await;
339 *storage_guard = None;
340 self.store.delete().await?;
341 info!("Token revoked and removed");
342 Ok(())
343 }
344}
345
346pub struct AuthConfigBuilder {
347 config: OAuthConfig,
348}
349
350impl AuthConfigBuilder {
351 pub fn new() -> Self {
352 Self {
353 config: OAuthConfig::default(),
354 }
355 }
356
357 pub fn client_id(mut self, id: impl Into<String>) -> Self {
358 self.config.client_id = id.into();
359 self
360 }
361
362 pub fn client_secret(mut self, secret: Option<String>) -> Self {
363 self.config.client_secret = secret.filter(|s| !s.is_empty());
365 self
366 }
367
368 pub async fn build(self) -> Result<GoogleAuth> {
369 GoogleAuth::new(self.config).await
370 }
371}
372
373impl Default for AuthConfigBuilder {
374 fn default() -> Self {
375 Self::new()
376 }
377}