use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::anyhow;
use crate::core::error::{GrrError, Result, json_error_detail};
use super::TokenStorage;
const DEVICE_CODE_URL: &str = "https://oauth2.googleapis.com/device/code";
const DEVICE_GRANT: &str = "urn:ietf:params:oauth:grant-type:device_code";
pub struct DeviceAuthChallenge {
pub verification_url: String,
pub user_code: String,
pub(crate) device_code: String,
pub(crate) interval: Duration,
pub(crate) deadline: Instant,
}
impl DeviceAuthChallenge {
pub fn retry_after(&self) -> Duration {
self.interval
}
pub fn is_expired(&self) -> bool {
Instant::now() >= self.deadline
}
}
impl super::GoogleAuth {
pub async fn request_device_code(&self) -> Result<DeviceAuthChallenge> {
let mut form = vec![
("client_id", self.config.client_id.clone()),
("scope", super::scopes_joined()),
];
if let Some(secret) = self.config.client_secret.clone().filter(|s| !s.is_empty()) {
form.push(("client_secret", secret));
}
let response = self
.http_client
.post(DEVICE_CODE_URL)
.form(&form)
.send()
.await
.map_err(GrrError::Http)?;
let status = response.status();
let body_text = response.text().await.map_err(GrrError::Http)?;
let body: serde_json::Value = serde_json::from_str(&body_text).map_err(|e| {
GrrError::Auth(
anyhow!("device endpoint returned {status}: unparseable body ({e})").into(),
)
})?;
if !status.is_success() {
return Err(GrrError::Auth(
anyhow!(
"device endpoint returned {status}: {}",
json_error_detail(&body)
)
.into(),
));
}
let device_code = body["device_code"]
.as_str()
.ok_or_else(|| GrrError::Auth(anyhow!("device endpoint omitted device_code").into()))?;
let user_code = body["user_code"]
.as_str()
.ok_or_else(|| GrrError::Auth(anyhow!("device endpoint omitted user_code").into()))?;
let verification_url = body
.get("verification_url")
.or_else(|| body.get("verification_uri"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
GrrError::Auth(anyhow!("device endpoint omitted verification_url").into())
})?;
let expires_in = body["expires_in"].as_u64().unwrap_or(1800);
let interval = body["interval"]
.as_u64()
.map(Duration::from_secs)
.filter(|d| !d.is_zero())
.unwrap_or_else(|| Duration::from_secs(5));
Ok(DeviceAuthChallenge {
verification_url: verification_url.to_string(),
user_code: user_code.to_string(),
device_code: device_code.to_string(),
interval,
deadline: Instant::now() + Duration::from_secs(expires_in),
})
}
pub async fn poll_device_code(
&self,
challenge: &mut DeviceAuthChallenge,
) -> Result<Option<TokenStorage>> {
let mut form = vec![
("client_id", self.config.client_id.clone()),
("device_code", challenge.device_code.clone()),
("grant_type", DEVICE_GRANT.to_string()),
];
if let Some(secret) = self.config.client_secret.clone().filter(|s| !s.is_empty()) {
form.push(("client_secret", secret));
}
let response = self
.http_client
.post(self.token_endpoint.as_str())
.form(&form)
.send()
.await
.map_err(GrrError::Http)?;
let body_text = response.text().await.map_err(GrrError::Http)?;
let body: serde_json::Value = serde_json::from_str(&body_text).map_err(|e| {
GrrError::Auth(anyhow!("token endpoint returned unparseable body ({e})").into())
})?;
if let Some(code) = body["error"].as_str() {
match code {
"authorization_pending" => return Ok(None),
"slow_down" => {
challenge.interval += Duration::from_secs(5);
return Ok(None);
}
_ => {
return Err(GrrError::Auth(
anyhow!("device poll failed: {}", json_error_detail(&body)).into(),
));
}
}
}
let storage = token_storage_from_response(&body, super::SCOPES)?;
*self.token_storage.write().await = Some(storage.clone());
self.save_token(&storage).await?;
Ok(Some(storage))
}
pub async fn login(&self) -> Result<TokenStorage> {
let _ = self.revoke().await;
let storage = self.run_oauth_flow().await?;
*self.token_storage.write().await = Some(storage.clone());
self.save_token(&storage).await?;
Ok(storage)
}
}
pub(crate) fn token_storage_from_response(
token_data: &serde_json::Value,
scopes: &[&str],
) -> Result<TokenStorage> {
let access_token = token_data["access_token"]
.as_str()
.ok_or_else(|| GrrError::Auth(anyhow!("No access token in response").into()))?
.to_string();
let expires_in = token_data["expires_in"].as_u64().unwrap_or(3600);
let refresh_token = token_data["refresh_token"].as_str().map(str::to_string);
let expires_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
+ expires_in;
Ok(TokenStorage {
access_token,
refresh_token,
expires_at,
token_type: "Bearer".to_string(),
scope: scopes.join(" "),
})
}