vtcode_auth/openai_chatgpt_oauth/
mod.rs1use anyhow::{Context, Result, anyhow, bail};
25use async_trait::async_trait;
26use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
27use fs2::FileExt;
28use reqwest::Client;
29use ring::rand::{SecureRandom, SystemRandom};
30use serde::{Deserialize, Serialize};
31use std::fmt;
32use std::fs;
33use std::fs::OpenOptions;
34use std::sync::{Arc, Mutex};
35use tokio::sync::Mutex as AsyncMutex;
36
37use crate::storage_paths::auth_storage_dir;
38use crate::{OpenAIAuthConfig, OpenAIPreferredMethod};
39
40pub use super::credentials::AuthCredentialsStoreMode;
41use super::pkce::PkceChallenge;
42#[cfg(test)]
43use crate::openai_refresh_policy::extract_error_code;
44use crate::openai_refresh_policy::{RefreshFailureAction, classify_refresh_failure};
45use crate::openai_session_storage::OpenAiSessionStorage;
46#[cfg(test)]
47use crate::openai_session_storage::{
48 decrypt_legacy_session as decrypt_session, encrypt_legacy_session as encrypt_session,
49 legacy_session_path as get_session_path,
50};
51
52const OPENAI_AUTH_URL: &str = "https://auth.openai.com/oauth/authorize";
53const OPENAI_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
54const DEFAULT_OPENAI_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
70const DEFAULT_OPENAI_ORIGINATOR: &str = "codex_cli_rs";
76const MAX_ERROR_BODY_BYTES: usize = 8 * 1024;
80const OPENAI_CALLBACK_PATH: &str = "/auth/callback";
81const OPENAI_REFRESH_LOCK_FILE: &str = "openai_chatgpt.refresh.lock";
82const REFRESH_INTERVAL_SECS: u64 = 8 * 60;
83const REFRESH_SKEW_SECS: u64 = 60;
84
85#[derive(Debug)]
97struct OAuthClientIdentity {
98 client_id: String,
99 originator: String,
100}
101
102fn resolve_oauth_client_identity() -> Result<OAuthClientIdentity> {
120 let custom_client_id = std::env::var("VTCODE_OPENAI_OAUTH_CLIENT_ID")
121 .ok()
122 .filter(|v| !v.trim().is_empty());
123 let custom_originator = std::env::var("VTCODE_OPENAI_OAUTH_ORIGINATOR")
124 .ok()
125 .filter(|v| !v.trim().is_empty());
126
127 match (custom_client_id, custom_originator) {
128 (Some(id), Some(originator)) => Ok(OAuthClientIdentity { client_id: id, originator }),
129 (Some(_), None) => bail!(
130 "VTCODE_OPENAI_OAUTH_CLIENT_ID is set but VTCODE_OPENAI_OAUTH_ORIGINATOR is not. \
131 The client ID and originator must be overridden together to form a coherent OAuth \
132 identity. Set VTCODE_OPENAI_OAUTH_ORIGINATOR to match your custom client ID, \
133 or unset VTCODE_OPENAI_OAUTH_CLIENT_ID to use the default Codex identity."
134 ),
135 (None, Some(_)) => bail!(
136 "VTCODE_OPENAI_OAUTH_ORIGINATOR is set but VTCODE_OPENAI_OAUTH_CLIENT_ID is not. \
137 The client ID and originator must be overridden together to form a coherent OAuth \
138 identity. Set VTCODE_OPENAI_OAUTH_CLIENT_ID to match your custom originator, \
139 or unset VTCODE_OPENAI_OAUTH_ORIGINATOR to use the default Codex identity."
140 ),
141 (None, None) => Ok(OAuthClientIdentity {
142 client_id: DEFAULT_OPENAI_CLIENT_ID.to_string(),
143 originator: DEFAULT_OPENAI_ORIGINATOR.to_string(),
144 }),
145 }
146}
147
148mod jwt;
149mod refresh;
150mod session;
151
152pub(crate) use jwt::{parse_jwt_claims, parse_jwt_exp};
153pub use refresh::{
154 clear_openai_chatgpt_session, clear_openai_chatgpt_session_with_mode, exchange_openai_chatgpt_code_for_tokens,
155 get_openai_chatgpt_auth_status, get_openai_chatgpt_auth_status_with_mode, load_openai_chatgpt_session,
156 load_openai_chatgpt_session_with_mode, refresh_openai_chatgpt_session_with_mode, save_openai_chatgpt_session,
157 save_openai_chatgpt_session_with_mode,
158};
159pub use session::{
160 OpenAIChatGptAuthHandle, OpenAIChatGptAuthStatus, OpenAIChatGptSession, OpenAIChatGptSessionProvenance,
161 OpenAIChatGptSessionRefresher, OpenAICredentialOverview, OpenAIResolvedAuth, OpenAIResolvedAuthSource,
162 generate_openai_oauth_state, get_openai_chatgpt_auth_url, parse_openai_chatgpt_manual_callback_input,
163 resolve_openai_auth, summarize_openai_credentials,
164};
165
166#[cfg(test)]
167pub(crate) use refresh::{
168 OpenAIRefreshResponse, acquire_refresh_lock, classify_refresh_status_error, merge_refresh_response,
169};
170#[cfg(test)]
171pub(crate) use session::active_api_bearer_token;
172
173pub(super) fn now_secs() -> u64 {
175 std::time::SystemTime::now()
176 .duration_since(std::time::UNIX_EPOCH)
177 .map(|duration| duration.as_secs())
178 .unwrap_or(0)
179}
180
181#[cfg(test)]
182mod tests;