use url::Url;
use crate::types::TokenResponse;
use crate::{OAuthConfig, OAuthFlow, OpenAIAuthError, Result, TokenSet};
#[derive(Clone)]
pub struct OAuthClient {
config: OAuthConfig,
}
impl OAuthClient {
pub fn new(config: OAuthConfig) -> Result<Self> {
Ok(Self { config })
}
pub fn start_flow(&self) -> Result<OAuthFlow> {
let state = crate::types::generate_random_state();
let (pkce_challenge, pkce_verifier) = crate::types::generate_pkce_pair();
let mut url = Url::parse(&self.config.auth_url)?;
url.query_pairs_mut()
.append_pair("response_type", "code")
.append_pair("client_id", &self.config.client_id)
.append_pair("redirect_uri", &self.config.redirect_uri)
.append_pair("scope", "openid profile email offline_access")
.append_pair("code_challenge", &pkce_challenge)
.append_pair("code_challenge_method", "S256")
.append_pair("state", &state)
.append_pair("id_token_add_organizations", "true")
.append_pair("codex_cli_simplified_flow", "true")
.append_pair("originator", "codex_cli_rs");
Ok(OAuthFlow {
authorization_url: url.to_string(),
pkce_verifier,
state,
})
}
pub fn extract_account_id(&self, access_token: &str) -> Result<String> {
crate::jwt::extract_account_id(access_token)
}
pub async fn exchange_code(&self, code: &str, verifier: &str) -> Result<TokenSet> {
let client = reqwest::Client::new();
let params = [
("grant_type", "authorization_code"),
("client_id", &self.config.client_id),
("code", code),
("code_verifier", verifier),
("redirect_uri", &self.config.redirect_uri),
];
let response = client
.post(&self.config.token_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.form(¶ms)
.send()
.await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
return Err(OpenAIAuthError::Http { status, body });
}
let token_response: TokenResponse = response.json().await?;
Ok(TokenSet::from(token_response))
}
pub async fn exchange_code_for_api_key(&self, code: &str, verifier: &str) -> Result<TokenSet> {
let mut tokens = self.exchange_code(code, verifier).await?;
let id_token = tokens.id_token.as_deref().ok_or_else(|| {
OpenAIAuthError::TokenExchange("missing id_token for api key exchange".to_string())
})?;
let api_key = self.obtain_api_key(id_token).await?;
tokens.api_key = Some(api_key);
Ok(tokens)
}
pub async fn obtain_api_key(&self, id_token: &str) -> Result<String> {
#[derive(serde::Deserialize)]
struct ExchangeResponse {
access_token: String,
}
let client = reqwest::Client::new();
let params = [
(
"grant_type",
"urn:ietf:params:oauth:grant-type:token-exchange",
),
("client_id", &self.config.client_id),
("requested_token", "openai-api-key"),
("subject_token", id_token),
(
"subject_token_type",
"urn:ietf:params:oauth:token-type:id_token",
),
];
let response = client
.post(&self.config.token_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.form(¶ms)
.send()
.await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
return Err(OpenAIAuthError::Http { status, body });
}
let exchange: ExchangeResponse = response.json().await?;
Ok(exchange.access_token)
}
pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenSet> {
let client = reqwest::Client::new();
let params = [
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
("client_id", &self.config.client_id),
];
let response = client
.post(&self.config.token_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.form(¶ms)
.send()
.await?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
return Err(OpenAIAuthError::ApiKeyExchange { status, body });
}
let token_response: TokenResponse = response.json().await?;
Ok(TokenSet::from(token_response))
}
}
impl Default for OAuthClient {
fn default() -> Self {
Self::new(OAuthConfig::default()).expect("Failed to create OAuth client with defaults")
}
}