From 8fc03130ef9e0eddcd626a8b07faf33838cdc923 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 10 Mar 2026 15:39:15 -0400 Subject: [PATCH] Fix OAuth callback path to /auth/callback and add login example The redirect_uri path must be /auth/callback (not /callback) to match what OpenAI's auth server expects for this client ID. Also use localhost instead of 127.0.0.1 to match the Codex CLI convention. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/arc-openai-oauth/examples/login.rs | 24 +++++++++++++++++++ lib/crates/arc-openai-oauth/src/lib.rs | 14 +++++------ 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 lib/crates/arc-openai-oauth/examples/login.rs diff --git a/lib/crates/arc-openai-oauth/examples/login.rs b/lib/crates/arc-openai-oauth/examples/login.rs new file mode 100644 index 000000000..b459c385e --- /dev/null +++ b/lib/crates/arc-openai-oauth/examples/login.rs @@ -0,0 +1,24 @@ +use arc_openai_oauth::{extract_account_id, run_browser_flow, DEFAULT_CLIENT_ID, DEFAULT_ISSUER}; + +#[tokio::main] +async fn main() { + match run_browser_flow(DEFAULT_ISSUER, DEFAULT_CLIENT_ID).await { + Ok(tokens) => { + println!("Login successful!"); + if let Some(account_id) = extract_account_id(&tokens) { + println!("Account ID: {account_id}"); + } + println!( + "Access token: {}...", + &tokens.access_token[..20.min(tokens.access_token.len())] + ); + if let Some(expires_in) = tokens.expires_in { + println!("Expires in: {expires_in}s"); + } + } + Err(e) => { + eprintln!("Login failed: {e}"); + std::process::exit(1); + } + } +} diff --git a/lib/crates/arc-openai-oauth/src/lib.rs b/lib/crates/arc-openai-oauth/src/lib.rs index 329f73523..bd6cbaa4a 100644 --- a/lib/crates/arc-openai-oauth/src/lib.rs +++ b/lib/crates/arc-openai-oauth/src/lib.rs @@ -306,7 +306,7 @@ pub async fn poll_device_flow( device: &DeviceAuthResponse, ) -> Result { let poll_url = format!("{issuer}/api/accounts/deviceauth/token"); - let redirect_uri = format!("http://127.0.0.1:{OAUTH_PORT}/callback"); + let redirect_uri = format!("http://localhost:{OAUTH_PORT}/auth/callback"); let mut attempt = 0u32; loop { @@ -365,7 +365,7 @@ pub async fn start_callback_server( port: u16, expected_state: String, ) -> Result<(u16, tokio::sync::oneshot::Receiver), String> { - let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{port}")) + let listener = tokio::net::TcpListener::bind(format!("localhost:{port}")) .await .map_err(|e| format!("Failed to bind callback server: {e}"))?; let actual_port = listener @@ -381,7 +381,7 @@ pub async fn start_callback_server( let expected_state = std::sync::Arc::new(expected_state); let app = axum::Router::new().route( - "/callback", + "/auth/callback", axum::routing::get( move |axum::extract::Query(params): axum::extract::Query| async move { if params.state != *expected_state { @@ -433,7 +433,7 @@ pub async fn start_callback_server( pub async fn run_browser_flow(issuer: &str, client_id: &str) -> Result { let pkce = generate_pkce(); let state = generate_state(); - let redirect_uri = format!("http://127.0.0.1:{OAUTH_PORT}/callback"); + let redirect_uri = format!("http://localhost:{OAUTH_PORT}/auth/callback"); let (_port, code_rx) = start_callback_server(OAUTH_PORT, state.clone()).await?; let auth_url = build_authorize_url(issuer, client_id, &redirect_uri, &pkce, &state); @@ -982,7 +982,7 @@ mod tests { let client = reqwest::Client::new(); client .get(format!( - "http://127.0.0.1:{port}/callback?code=abc&state=test-state" + "http://localhost:{port}/auth/callback?code=abc&state=test-state" )) .send() .await @@ -1001,7 +1001,7 @@ mod tests { let client = reqwest::Client::new(); let resp = client .get(format!( - "http://127.0.0.1:{port}/callback?code=abc&state=wrong-state" + "http://localhost:{port}/auth/callback?code=abc&state=wrong-state" )) .send() .await @@ -1019,7 +1019,7 @@ mod tests { let client = reqwest::Client::new(); let resp = client .get(format!( - "http://127.0.0.1:{port}/callback?code=abc&state=test-state" + "http://localhost:{port}/auth/callback?code=abc&state=test-state" )) .send() .await