mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
## What Adds an optional hosted login-success redirect path for app-server login requests. - Keeps the existing localhost success page as the default. - Lets app-server callers opt into a hosted success page with an optional protocol field. - Persists credentials before redirecting to the hosted success page. - Keeps org setup and existing CLI/device-code login flows on the local success page. - Accepts an optional typed `appBrand` value and forwards it to the hosted page as `app_brand` so web can select the correct asset. - Generates the app-server protocol schema updates for the new optional fields. ## Why This supports the hosted Codex login success page rollout without changing existing login behavior by default. The Codex Apps frontend can gate the opt-in with Statsig after the hosted web page. ## Rollout safety - Old callers omit the new field and continue using localhost. - New callers talking to old app-server builds remain safe because the Codex Apps side treats the field as optional and defaults the flag off. - Missing brand values default to Codex. - The hosted redirect always uses the app-login source so the hosted page can reopen Codex; the existing streamlined-login visual flag remains separate. ## Validation - `just fmt` - `just fix -p codex-login -p codex-app-server-protocol -p codex-app-server -p codex-app-server-test-client -p codex-tui` - `just test -p codex-login` - `just test -p codex-app-server-protocol` - `just write-app-server-schema` - `git diff HEAD --check` The focused login and protocol run passed all 380 tests. I also started the broader `just test -p codex-app-server`; it compiled successfully, then many tests failed on this machine because spawned test servers tried to use the ambient `/Users/rafaelj/.codex/sqlite` state DB, which is read-only in this sandbox. I stopped that run after confirming the failures shared that environment issue.
144 lines
4.4 KiB
Rust
144 lines
4.4 KiB
Rust
use base64::Engine;
|
|
use serde_json::Value as JsonValue;
|
|
use url::Url;
|
|
|
|
use crate::server::DEFAULT_ISSUER;
|
|
|
|
pub const CODEX_OPEN_APP_URL: &str = "https://chatgpt.com/codex/open-app";
|
|
|
|
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
|
pub enum LoginSuccessPage {
|
|
#[default]
|
|
Local,
|
|
Hosted {
|
|
url: Url,
|
|
app_brand: LoginSuccessPageBrand,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
pub enum LoginSuccessPageBrand {
|
|
Codex,
|
|
Chatgpt,
|
|
}
|
|
|
|
impl LoginSuccessPageBrand {
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Codex => "codex",
|
|
Self::Chatgpt => "chatgpt",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
pub(crate) enum LoginSuccessRedirect {
|
|
Local(String),
|
|
Hosted(String),
|
|
}
|
|
|
|
pub(crate) fn compose_success_url(
|
|
port: u16,
|
|
issuer: &str,
|
|
id_token: &str,
|
|
access_token: &str,
|
|
codex_streamlined_login: bool,
|
|
login_success_page: &LoginSuccessPage,
|
|
) -> LoginSuccessRedirect {
|
|
let token_claims = jwt_auth_claims(id_token);
|
|
|
|
let org_id = token_claims
|
|
.get("organization_id")
|
|
.and_then(|value| value.as_str())
|
|
.unwrap_or("");
|
|
let project_id = token_claims
|
|
.get("project_id")
|
|
.and_then(|value| value.as_str())
|
|
.unwrap_or("");
|
|
let completed_onboarding = token_claims
|
|
.get("completed_platform_onboarding")
|
|
.and_then(JsonValue::as_bool)
|
|
.unwrap_or(false);
|
|
let is_org_owner = token_claims
|
|
.get("is_org_owner")
|
|
.and_then(JsonValue::as_bool)
|
|
.unwrap_or(false);
|
|
let needs_setup = !completed_onboarding && is_org_owner;
|
|
if !needs_setup && let LoginSuccessPage::Hosted { url, app_brand } = login_success_page {
|
|
let mut success_url = url.clone();
|
|
success_url.set_query(None);
|
|
success_url
|
|
.query_pairs_mut()
|
|
.append_pair("source", "login")
|
|
.append_pair("app_brand", app_brand.as_str());
|
|
return LoginSuccessRedirect::Hosted(success_url.into());
|
|
}
|
|
|
|
let access_claims = jwt_auth_claims(access_token);
|
|
let plan_type = access_claims
|
|
.get("chatgpt_plan_type")
|
|
.and_then(|value| value.as_str())
|
|
.unwrap_or("");
|
|
let platform_url = if issuer == DEFAULT_ISSUER {
|
|
"https://platform.openai.com"
|
|
} else {
|
|
"https://platform.api.openai.org"
|
|
};
|
|
let mut params = vec![
|
|
("id_token", id_token.to_string()),
|
|
("needs_setup", needs_setup.to_string()),
|
|
("org_id", org_id.to_string()),
|
|
("project_id", project_id.to_string()),
|
|
("plan_type", plan_type.to_string()),
|
|
("platform_url", platform_url.to_string()),
|
|
];
|
|
if codex_streamlined_login {
|
|
params.push(("codex_streamlined_login", "true".to_string()));
|
|
}
|
|
let query = params
|
|
.into_iter()
|
|
.map(|(key, value)| format!("{key}={}", urlencoding::encode(&value)))
|
|
.collect::<Vec<_>>()
|
|
.join("&");
|
|
LoginSuccessRedirect::Local(format!("http://localhost:{port}/success?{query}"))
|
|
}
|
|
|
|
pub(crate) fn jwt_auth_claims(jwt: &str) -> serde_json::Map<String, serde_json::Value> {
|
|
let mut parts = jwt.split('.');
|
|
let (_header, payload, _signature) = match (parts.next(), parts.next(), parts.next()) {
|
|
(Some(header), Some(payload), Some(signature))
|
|
if !header.is_empty() && !payload.is_empty() && !signature.is_empty() =>
|
|
{
|
|
(header, payload, signature)
|
|
}
|
|
_ => {
|
|
eprintln!("Invalid JWT format while extracting claims");
|
|
return serde_json::Map::new();
|
|
}
|
|
};
|
|
match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload) {
|
|
Ok(bytes) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
|
|
Ok(mut value) => {
|
|
if let Some(claims) = value
|
|
.get_mut("https://api.openai.com/auth")
|
|
.and_then(JsonValue::as_object_mut)
|
|
{
|
|
return claims.clone();
|
|
}
|
|
eprintln!("JWT payload missing expected 'https://api.openai.com/auth' object");
|
|
}
|
|
Err(error) => {
|
|
eprintln!("Failed to parse JWT JSON payload: {error}");
|
|
}
|
|
},
|
|
Err(error) => {
|
|
eprintln!("Failed to base64url-decode JWT payload: {error}");
|
|
}
|
|
}
|
|
serde_json::Map::new()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "success_page_tests.rs"]
|
|
mod tests;
|