rusty_commit/auth/
vercel_oauth.rs1use anyhow::{Context, Result};
2use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::sync::Arc;
7use std::time::{Duration, SystemTime};
8use tokio::sync::Mutex;
9use tokio::time::sleep;
10
11#[allow(dead_code)]
13pub const VERCEL_AUTHORIZE_URL: &str = "https://vercel.com/oauth/authorize";
14pub const VERCEL_TOKEN_URL: &str = "https://api.vercel.com/oauth/token";
15#[allow(dead_code)]
16pub const VERCEL_API_URL: &str = "https://api.vercel.com";
17
18#[derive(Debug, Serialize)]
19#[allow(dead_code)]
20struct VercelTokenRequest {
21 grant_type: String,
22 code: String,
23 redirect_uri: String,
24 client_id: String,
25 code_verifier: String,
26}
27
28#[derive(Debug, Serialize)]
29#[allow(dead_code)]
30struct VercelRefreshTokenRequest {
31 grant_type: String,
32 refresh_token: String,
33 client_id: String,
34}
35
36#[derive(Debug, Deserialize)]
37#[allow(dead_code)]
38pub struct VercelTokenResponse {
39 pub access_token: String,
40 pub refresh_token: Option<String>,
41 pub token_type: String,
42 pub expires_in: Option<u64>,
43 pub scope: Option<String>,
44}
45
46#[derive(Debug, Deserialize)]
47struct VercelErrorResponse {
48 error: String,
49 error_description: Option<String>,
50}
51
52#[allow(dead_code)]
54pub struct VercelOAuthClient {
55 client: Client,
56 client_id: String,
57 #[allow(dead_code)]
58 redirect_uri: String,
59}
60
61impl Default for VercelOAuthClient {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[allow(dead_code)]
68impl VercelOAuthClient {
69 pub fn new() -> Self {
70 Self {
71 client: Client::new(),
72 client_id: "rusty-commit-cli".to_string(), redirect_uri: "http://localhost:1456/auth/callback".to_string(),
74 }
75 }
76
77 fn generate_pkce() -> Result<(String, String)> {
79 let mut bytes = [0u8; 32];
80 generate_random_bytes(&mut bytes)?;
81 let verifier = URL_SAFE_NO_PAD.encode(bytes);
82
83 let mut hasher = Sha256::new();
84 hasher.update(verifier.as_bytes());
85 let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
86
87 Ok((verifier, challenge))
88 }
89
90 fn generate_state() -> String {
92 let mut bytes = [0u8; 32];
93 let _ = generate_random_bytes(&mut bytes);
95 URL_SAFE_NO_PAD.encode(bytes)
96 }
97
98 pub fn get_authorization_url(&self) -> Result<(String, String)> {
100 let (verifier, challenge) = Self::generate_pkce()?;
101 let state = Self::generate_state();
102
103 let params = [
104 ("client_id", self.client_id.as_str()),
105 ("redirect_uri", self.redirect_uri.as_str()),
106 ("response_type", "code"),
107 ("scope", "openid profile email"),
108 ("state", state.as_str()),
109 ("code_challenge", challenge.as_str()),
110 ("code_challenge_method", "S256"),
111 ];
112
113 let query = serde_urlencoded::to_string(params).context("Failed to encode OAuth params")?;
114 let auth_url = format!("{}?{}", VERCEL_AUTHORIZE_URL, query);
115
116 Ok((auth_url, verifier))
117 }
118
119 pub async fn start_callback_server(&self, verifier: String) -> Result<VercelTokenResponse> {
121 use warp::Filter;
122
123 let code = Arc::new(Mutex::new(None));
124 let code_clone = code.clone();
125
126 let callback = warp::path("auth")
127 .and(warp::path("callback"))
128 .and(warp::query::<std::collections::HashMap<String, String>>())
129 .map(move |params: std::collections::HashMap<String, String>| {
130 if let Some(auth_code) = params.get("code") {
131 let mut code_lock = code_clone.blocking_lock();
132 *code_lock = Some(auth_code.clone());
133 }
134 warp::reply::html(r#"<!DOCTYPE html><html><head><title>Authenticated!</title></head><body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #000;"><div style="background: white; padding: 2rem; border-radius: 8px; text-align: center;"><h1 style="color: #000;">Authentication Successful!</h1><p>You can close this window.</p></div></body></html>"#)
135 });
136
137 let server = warp::serve(callback).bind(([127, 0, 0, 1], 1456));
138 let server_handle = tokio::spawn(server);
139
140 let start = std::time::SystemTime::now();
141 let timeout = Duration::from_secs(300);
142
143 loop {
144 if let Some(auth_code) = &*code.lock().await {
145 let token = self.exchange_code_for_token(auth_code, &verifier).await?;
146 server_handle.abort();
147 return Ok(token);
148 }
149
150 if SystemTime::now().duration_since(start)? > timeout {
151 server_handle.abort();
152 anyhow::bail!("Authentication timeout");
153 }
154
155 sleep(Duration::from_millis(100)).await;
156 }
157 }
158
159 async fn exchange_code_for_token(
161 &self,
162 code: &str,
163 verifier: &str,
164 ) -> Result<VercelTokenResponse> {
165 let params = [
166 ("grant_type", "authorization_code"),
167 ("code", code),
168 ("redirect_uri", self.redirect_uri.as_str()),
169 ("client_id", self.client_id.as_str()),
170 ("code_verifier", verifier),
171 ];
172
173 let response = self
174 .client
175 .post(VERCEL_TOKEN_URL)
176 .form(¶ms)
177 .send()
178 .await
179 .context("Failed to exchange code for token")?;
180
181 if response.status().is_success() {
182 response
183 .json::<VercelTokenResponse>()
184 .await
185 .context("Failed to parse token response")
186 } else {
187 let error: VercelErrorResponse = response.json().await?;
188 anyhow::bail!(
189 "Token exchange failed: {} - {}",
190 error.error,
191 error.error_description.unwrap_or_default()
192 )
193 }
194 }
195
196 #[allow(dead_code)]
198 pub async fn refresh_token(&self, refresh_token: &str) -> Result<VercelTokenResponse> {
199 let params = [
200 ("grant_type", "refresh_token"),
201 ("refresh_token", refresh_token),
202 ("client_id", self.client_id.as_str()),
203 ];
204
205 let response = self
206 .client
207 .post(VERCEL_TOKEN_URL)
208 .form(¶ms)
209 .send()
210 .await
211 .context("Failed to refresh token")?;
212
213 if response.status().is_success() {
214 response
215 .json::<VercelTokenResponse>()
216 .await
217 .context("Failed to parse refresh token response")
218 } else {
219 let error: VercelErrorResponse = response.json().await?;
220 anyhow::bail!(
221 "Token refresh failed: {} - {}",
222 error.error,
223 error.error_description.unwrap_or_default()
224 )
225 }
226 }
227}
228
229#[allow(dead_code)]
231fn generate_random_bytes(dest: &mut [u8]) -> Result<()> {
232 use rand::RngCore;
233 let mut rng = rand::rng();
234 rng.fill_bytes(dest);
235 Ok(())
236}