grr_cli/core/auth/
device.rs1use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12use anyhow::anyhow;
13
14use crate::core::error::{GrrError, Result, json_error_detail};
15
16use super::TokenStorage;
17
18const DEVICE_CODE_URL: &str = "https://oauth2.googleapis.com/device/code";
19const DEVICE_GRANT: &str = "urn:ietf:params:oauth:grant-type:device_code";
20
21pub struct DeviceAuthChallenge {
24 pub verification_url: String,
26 pub user_code: String,
28 pub(crate) device_code: String,
29 pub(crate) interval: Duration,
30 pub(crate) deadline: Instant,
31}
32
33impl DeviceAuthChallenge {
34 pub fn retry_after(&self) -> Duration {
36 self.interval
37 }
38
39 pub fn is_expired(&self) -> bool {
41 Instant::now() >= self.deadline
42 }
43}
44
45impl super::GoogleAuth {
46 pub async fn request_device_code(&self) -> Result<DeviceAuthChallenge> {
48 let mut form = vec![
49 ("client_id", self.config.client_id.clone()),
50 ("scope", super::scopes_joined()),
51 ];
52 if let Some(secret) = self.config.client_secret.clone().filter(|s| !s.is_empty()) {
53 form.push(("client_secret", secret));
54 }
55
56 let response = self
57 .http_client
58 .post(DEVICE_CODE_URL)
59 .form(&form)
60 .send()
61 .await
62 .map_err(GrrError::Http)?;
63
64 let status = response.status();
65 let body_text = response.text().await.map_err(GrrError::Http)?;
66 let body: serde_json::Value = serde_json::from_str(&body_text).map_err(|e| {
67 GrrError::Auth(
68 anyhow!("device endpoint returned {status}: unparseable body ({e})").into(),
69 )
70 })?;
71
72 if !status.is_success() {
73 return Err(GrrError::Auth(
74 anyhow!(
75 "device endpoint returned {status}: {}",
76 json_error_detail(&body)
77 )
78 .into(),
79 ));
80 }
81
82 let device_code = body["device_code"]
83 .as_str()
84 .ok_or_else(|| GrrError::Auth(anyhow!("device endpoint omitted device_code").into()))?;
85 let user_code = body["user_code"]
86 .as_str()
87 .ok_or_else(|| GrrError::Auth(anyhow!("device endpoint omitted user_code").into()))?;
88 let verification_url = body
89 .get("verification_url")
90 .or_else(|| body.get("verification_uri"))
91 .and_then(serde_json::Value::as_str)
92 .ok_or_else(|| {
93 GrrError::Auth(anyhow!("device endpoint omitted verification_url").into())
94 })?;
95
96 let expires_in = body["expires_in"].as_u64().unwrap_or(1800);
97 let interval = body["interval"]
98 .as_u64()
99 .map(Duration::from_secs)
100 .filter(|d| !d.is_zero())
101 .unwrap_or_else(|| Duration::from_secs(5));
102
103 Ok(DeviceAuthChallenge {
104 verification_url: verification_url.to_string(),
105 user_code: user_code.to_string(),
106 device_code: device_code.to_string(),
107 interval,
108 deadline: Instant::now() + Duration::from_secs(expires_in),
109 })
110 }
111
112 pub async fn poll_device_code(
116 &self,
117 challenge: &mut DeviceAuthChallenge,
118 ) -> Result<Option<TokenStorage>> {
119 let (_, body) = self
122 .token_request(vec![
123 ("client_id", self.config.client_id.clone()),
124 ("device_code", challenge.device_code.clone()),
125 ("grant_type", DEVICE_GRANT.to_string()),
126 ])
127 .await?;
128
129 if let Some(code) = body["error"].as_str() {
130 match code {
131 "authorization_pending" => return Ok(None),
132 "slow_down" => {
133 challenge.interval += Duration::from_secs(5);
134 return Ok(None);
135 }
136 _ => {
137 return Err(GrrError::Auth(
138 anyhow!("device poll failed: {}", json_error_detail(&body)).into(),
139 ));
140 }
141 }
142 }
143
144 let storage = token_storage_from_response(&body, super::scopes())?;
145 *self.token_storage.write().await = Some(storage.clone());
146 self.save_token(&storage).await?;
147 Ok(Some(storage))
148 }
149
150 pub async fn login(&self) -> Result<TokenStorage> {
153 let _ = self.revoke().await;
155 let storage = self.run_oauth_flow().await?;
156 *self.token_storage.write().await = Some(storage.clone());
157 self.save_token(&storage).await?;
158 Ok(storage)
159 }
160}
161
162pub(crate) fn token_storage_from_response(
165 token_data: &serde_json::Value,
166 scopes: &[String],
167) -> Result<TokenStorage> {
168 let access_token = token_data["access_token"]
169 .as_str()
170 .ok_or_else(|| GrrError::Auth(anyhow!("No access token in response").into()))?
171 .to_string();
172
173 let expires_in = token_data["expires_in"].as_u64().unwrap_or(3600);
174 let refresh_token = token_data["refresh_token"].as_str().map(str::to_string);
175
176 let expires_at = SystemTime::now()
177 .duration_since(UNIX_EPOCH)
178 .map(|d| d.as_secs())
179 .unwrap_or(0)
180 + expires_in;
181
182 Ok(TokenStorage {
183 access_token,
184 refresh_token,
185 expires_at,
186 token_type: "Bearer".to_string(),
187 scope: scopes.join(" "),
188 })
189}