This commit is contained in:
Eason Goodale
2025-08-11 18:29:14 -07:00
parent 90db5317d7
commit f7025fc317
5 changed files with 59 additions and 26 deletions

View File

@@ -79,9 +79,7 @@ pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result<SpawnedLog
}
/// Entrypoint used by the CLI to run the local login server.
pub async fn login_with_chatgpt(
codex_home: &Path,
) -> std::io::Result<()> {
pub async fn login_with_chatgpt(codex_home: &Path) -> std::io::Result<()> {
let client_id = std::env::var("CODEX_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())

View File

@@ -267,8 +267,14 @@ fn update_tokens_preserves_id_token_as_string() {
// Build a valid-looking JWT (URL-safe base64 header.payload.signature)
#[derive(Serialize)]
struct Header { alg: &'static str, typ: &'static str }
let header = Header { alg: "none", typ: "JWT" };
struct Header {
alg: &'static str,
typ: &'static str,
}
let header = Header {
alg: "none",
typ: "JWT",
};
let payload = serde_json::json!({});
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
let header_b64 = b64(&serde_json::to_vec(&header).unwrap());

View File

@@ -18,7 +18,8 @@ use crate::auth_store::write_new_auth_json;
use crate::pkce::generate_pkce;
use crate::success_url::build_success_url;
use crate::token_data::extract_login_context_from_tokens;
use tracing::{error, trace};
use tracing::error;
use tracing::trace;
pub const DEFAULT_PORT: u16 = 1455;
pub const DEFAULT_ISSUER: &str = "https://auth.openai.com";
@@ -29,7 +30,7 @@ pub const LOGIN_ERROR_HTML: &str = include_str!("./error_page.html");
fn render_error_html(message: &str) -> String {
LOGIN_ERROR_HTML.replace(
"%%MESSAGE%%",
&html_escape::encode_text(message).to_string(),
html_escape::encode_text(message).as_ref(),
)
}
@@ -188,8 +189,12 @@ pub fn run_local_login_server_with_options(mut opts: LoginServerOptions) -> std:
// Preserve explicit error messages for tests
if params.get("state").map(|s| s.as_str()) != Some(state.as_str()) {
let mut resp = Response::from_string(render_error_html("State parameter mismatch")).with_status_code(400);
if let Ok(h) = Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) {
let mut resp =
Response::from_string(render_error_html("State parameter mismatch"))
.with_status_code(400);
if let Ok(h) =
Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
{
resp.add_header(h);
}
if let Err(e) = request.respond(resp) {
@@ -199,8 +204,12 @@ pub fn run_local_login_server_with_options(mut opts: LoginServerOptions) -> std:
}
let code_opt = params.get("code").map(|s| s.as_str());
if code_opt.map(|s| s.is_empty()).unwrap_or(true) {
let mut resp = Response::from_string(render_error_html("Missing authorization code")).with_status_code(400);
if let Ok(h) = Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) {
let mut resp =
Response::from_string(render_error_html("Missing authorization code"))
.with_status_code(400);
if let Ok(h) =
Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..])
{
resp.add_header(h);
}
if let Err(e) = request.respond(resp) {
@@ -231,8 +240,13 @@ pub fn run_local_login_server_with_options(mut opts: LoginServerOptions) -> std:
}
}
Err(_) => {
let mut resp = Response::from_string(render_error_html("Token exchange failed")).with_status_code(500);
if let Ok(h) = Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) {
let mut resp =
Response::from_string(render_error_html("Token exchange failed"))
.with_status_code(500);
if let Ok(h) = Header::from_bytes(
&b"Content-Type"[..],
&b"text/html; charset=utf-8"[..],
) {
resp.add_header(h);
}
if let Err(e) = request.respond(resp) {
@@ -242,9 +256,9 @@ pub fn run_local_login_server_with_options(mut opts: LoginServerOptions) -> std:
}
}
_ => {
if let Err(e) = request.respond(
Response::from_string("Endpoint not supported").with_status_code(404),
) {
if let Err(e) = request
.respond(Response::from_string("Endpoint not supported").with_status_code(404))
{
error!("failed to respond 404: {e}");
}
}

View File

@@ -1,12 +1,12 @@
use tempfile::tempdir;
#[tokio::test]
async fn writes_api_key_and_loads_auth() {
let dir = tempdir().unwrap();
codex_login::login_with_api_key(dir.path(), "sk-test-key").unwrap();
let auth = codex_login::CodexAuth::from_codex_home(dir.path())
.unwrap()
.unwrap();
async fn writes_api_key_and_loads_auth() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
codex_login::login_with_api_key(dir.path(), "sk-test-key")?;
let auth = codex_login::CodexAuth::from_codex_home(dir.path())?
.ok_or_else(|| std::io::Error::other("expected Some(auth)"))?;
assert_eq!(auth.mode, codex_login::AuthMode::ApiKey);
assert_eq!(auth.get_token().await.unwrap().as_str(), "sk-test-key");
assert_eq!(auth.get_token().await?.as_str(), "sk-test-key");
Ok(())
}

View File

@@ -384,7 +384,12 @@ async fn login_server_state_mismatch() {
let (status, body, content_type) = http_get_with_ct(&cb_url);
assert_eq!(status, 400);
assert!(body.contains("State parameter mismatch"));
assert!(content_type.unwrap_or_default().to_ascii_lowercase().starts_with("text/html"));
assert!(
content_type
.unwrap_or_default()
.to_ascii_lowercase()
.starts_with("text/html")
);
// Stop server
let _ = ureq::get(&format!("http://127.0.0.1:{port}/success")).call();
@@ -410,7 +415,12 @@ async fn login_server_missing_code() {
let (status, body, content_type) = http_get_with_ct(&cb_url);
assert_eq!(status, 400);
assert!(body.contains("Missing authorization code"));
assert!(content_type.unwrap_or_default().to_ascii_lowercase().starts_with("text/html"));
assert!(
content_type
.unwrap_or_default()
.to_ascii_lowercase()
.starts_with("text/html")
);
let _ = ureq::get(&format!("http://127.0.0.1:{port}/success")).call();
handle.join().unwrap().unwrap();
}
@@ -431,7 +441,12 @@ async fn login_server_token_exchange_error() {
let (status, body, content_type) = http_get_with_ct(&cb_url);
assert_eq!(status, 500);
assert!(body.contains("Token exchange failed"));
assert!(content_type.unwrap_or_default().to_ascii_lowercase().starts_with("text/html"));
assert!(
content_type
.unwrap_or_default()
.to_ascii_lowercase()
.starts_with("text/html")
);
let _ = ureq::get(&format!("http://127.0.0.1:{port}/success")).call();
handle.join().unwrap().unwrap();
}