Skip to main content

rusty_commit/auth/
codex_oauth.rs

1use 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// OpenAI Codex OAuth endpoints (ChatGPT Pro/Plus)
12pub const CODEX_AUTHORIZE_URL: &str = "https://auth.openai.com/oauth/authorize";
13pub const CODEX_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
14pub const CODEX_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
15pub const CODEX_REDIRECT_URI: &str = "http://localhost:1455/auth/callback";
16
17#[derive(Debug, Deserialize)]
18#[allow(dead_code)]
19pub struct CodeXTokenResponse {
20    pub id_token: String,
21    pub access_token: String,
22    pub refresh_token: String,
23    pub expires_in: Option<u64>,
24    pub token_type: String,
25}
26
27#[derive(Debug, Serialize)]
28#[allow(dead_code)]
29struct CodeXTokenRequest {
30    grant_type: String,
31    code: String,
32    redirect_uri: String,
33    client_id: String,
34    code_verifier: String,
35}
36
37#[derive(Debug, Serialize)]
38#[allow(dead_code)]
39struct CodeXRefreshTokenRequest {
40    grant_type: String,
41    refresh_token: String,
42    client_id: String,
43}
44
45#[derive(Debug, Deserialize)]
46struct CodeXErrorResponse {
47    error: String,
48    error_description: Option<String>,
49}
50
51/// OAuth client for OpenAI Codex authentication (ChatGPT Pro/Plus)
52pub struct CodexOAuthClient {
53    client: Client,
54    client_id: String,
55    redirect_uri: String,
56}
57
58impl Default for CodexOAuthClient {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl CodexOAuthClient {
65    pub fn new() -> Self {
66        Self {
67            client: Client::new(),
68            client_id: CODEX_CLIENT_ID.to_string(),
69            redirect_uri: CODEX_REDIRECT_URI.to_string(),
70        }
71    }
72
73    /// Generate PKCE challenge and verifier
74    fn generate_pkce() -> Result<(String, String)> {
75        // Generate random verifier (43 characters as per RFC 7636)
76        let mut bytes = [0u8; 32];
77        generate_random_bytes(&mut bytes)?;
78        let verifier = URL_SAFE_NO_PAD.encode(bytes);
79
80        // Generate challenge from verifier
81        let mut hasher = Sha256::new();
82        hasher.update(verifier.as_bytes());
83        let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
84
85        Ok((verifier, challenge))
86    }
87
88    /// Generate random state for CSRF protection
89    fn generate_state() -> String {
90        let mut bytes = [0u8; 32];
91        generate_random_bytes(&mut bytes).unwrap_or_default();
92        URL_SAFE_NO_PAD.encode(bytes)
93    }
94
95    /// Build authorization URL with PKCE
96    pub fn get_authorization_url(&self) -> Result<(String, String)> {
97        let (verifier, challenge) = Self::generate_pkce()?;
98        let state = Self::generate_state();
99
100        let params = [
101            ("client_id", self.client_id.as_str()),
102            ("redirect_uri", self.redirect_uri.as_str()),
103            ("response_type", "code"),
104            ("scope", "openid profile email offline_access"),
105            ("state", state.as_str()),
106            ("code_challenge", challenge.as_str()),
107            ("code_challenge_method", "S256"),
108            ("id_token_add_organizations", "true"),
109            ("codex_cli_simplified_flow", "true"),
110            ("originator", "rusty-commit"),
111        ];
112
113        let query = serde_urlencoded::to_string(params).context("Failed to encode OAuth params")?;
114        let auth_url = format!("{}?{}", CODEX_AUTHORIZE_URL, query);
115
116        Ok((auth_url, verifier))
117    }
118
119    /// Start local server to receive OAuth callback
120    pub async fn start_callback_server(&self, verifier: String) -> Result<CodeXTokenResponse> {
121        use warp::Filter;
122
123        let code = Arc::new(Mutex::new(None));
124        let code_clone = code.clone();
125
126        // Create callback route
127        let callback = warp::path("auth")
128            .and(warp::path("callback"))
129            .and(warp::query::<std::collections::HashMap<String, String>>())
130            .map(move |params: std::collections::HashMap<String, String>| {
131                if let Some(auth_code) = params.get("code") {
132                    let mut code_lock = code_clone.blocking_lock();
133                    *code_lock = Some(auth_code.clone());
134                }
135
136                warp::reply::html(r#"
137                <!DOCTYPE html>
138                <html>
139                <head>
140                    <title>Authentication Successful</title>
141                    <style>
142                        body {
143                            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
144                            display: flex;
145                            justify-content: center;
146                            align-items: center;
147                            height: 100vh;
148                            margin: 0;
149                            background: linear-gradient(135deg, #10a37f 0%, #1a7f64 100%);
150                        }
151                        .container {
152                            background: white;
153                            padding: 3rem;
154                            border-radius: 12px;
155                            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
156                            text-align: center;
157                            max-width: 400px;
158                        }
159                        h1 { color: #1a7f64; margin-bottom: 1rem; }
160                        p { color: #666; line-height: 1.6; }
161                        .check {
162                            width: 60px;
163                            height: 60px;
164                            margin: 0 auto 1.5rem;
165                            background: #10a37f;
166                            border-radius: 50%;
167                            display: flex;
168                            align-items: center;
169                            justify-content: center;
170                        }
171                        .check::after {
172                            content: '✓';
173                            color: white;
174                            font-size: 30px;
175                            font-weight: bold;
176                        }
177                    </style>
178                </head>
179                <body>
180                    <div class="container">
181                        <div class="check"></div>
182                        <h1>Authentication Successful!</h1>
183                        <p>You can now close this window and return to your terminal.</p>
184                    </div>
185                </body>
186                </html>
187                "#)
188            });
189
190        // Start server in background
191        let server = warp::serve(callback).bind(([127, 0, 0, 1], 1455));
192        let server_handle = tokio::spawn(server);
193
194        // Wait for code (with timeout)
195        let start = SystemTime::now();
196        let timeout = Duration::from_secs(300); // 5 minutes
197
198        loop {
199            if let Some(auth_code) = &*code.lock().await {
200                // Exchange code for token
201                let token = self.exchange_code_for_token(auth_code, &verifier).await?;
202                server_handle.abort();
203                return Ok(token);
204            }
205
206            if SystemTime::now().duration_since(start)? > timeout {
207                server_handle.abort();
208                anyhow::bail!("Authentication timeout - no response received");
209            }
210
211            sleep(Duration::from_millis(100)).await;
212        }
213    }
214
215    /// Exchange authorization code for access token
216    async fn exchange_code_for_token(
217        &self,
218        code: &str,
219        verifier: &str,
220    ) -> Result<CodeXTokenResponse> {
221        let params = [
222            ("grant_type", "authorization_code"),
223            ("code", code),
224            ("redirect_uri", &self.redirect_uri),
225            ("client_id", &self.client_id),
226            ("code_verifier", verifier),
227        ];
228
229        let response = self
230            .client
231            .post(CODEX_TOKEN_URL)
232            .form(&params)
233            .send()
234            .await
235            .context("Failed to exchange code for token")?;
236
237        if response.status().is_success() {
238            response
239                .json::<CodeXTokenResponse>()
240                .await
241                .context("Failed to parse token response")
242        } else {
243            let error: CodeXErrorResponse = response.json().await?;
244            anyhow::bail!(
245                "Token exchange failed: {} - {}",
246                error.error,
247                error.error_description.unwrap_or_default()
248            )
249        }
250    }
251
252    /// Refresh an access token
253    #[allow(dead_code)]
254    pub async fn refresh_token(&self, refresh_token: &str) -> Result<CodeXTokenResponse> {
255        let params = [
256            ("grant_type", "refresh_token"),
257            ("refresh_token", refresh_token),
258            ("client_id", &self.client_id),
259        ];
260
261        let response = self
262            .client
263            .post(CODEX_TOKEN_URL)
264            .form(&params)
265            .send()
266            .await
267            .context("Failed to refresh token")?;
268
269        if response.status().is_success() {
270            response
271                .json::<CodeXTokenResponse>()
272                .await
273                .context("Failed to parse refresh token response")
274        } else {
275            let error: CodeXErrorResponse = response.json().await?;
276            anyhow::bail!(
277                "Token refresh failed: {} - {}",
278                error.error,
279                error.error_description.unwrap_or_default()
280            )
281        }
282    }
283}
284
285// Generate random bytes
286fn generate_random_bytes(dest: &mut [u8]) -> Result<()> {
287    use rand::RngCore;
288    let mut rng = rand::rng();
289    rng.fill_bytes(dest);
290    Ok(())
291}