ChatGPT 登录与认证
codex 跑模型前要拿到 OpenAI 凭据。这一层把"用户敲 codex login"到"auth.json 写到 $CODEX_HOME"之间所有事做完:浏览器 OAuth、PKCE、device code、API key、access token 五种入口共用同一份 CodexAuth 枚举和 AuthManager 状态机。login/ crate 负责纯认证协议,cli/src/login.rs 负责把协议产物落到磁盘并打 stderr 反馈。
职责
- 多种凭据形态统一:
CodexAuth枚举覆盖 API key、ChatGPT OAuth、外部 ChatGPT tokens、agent identity JWT、personal access token (PAT)、Bedrock API key 七种模式。codex-rs/login/src/auth/manager.rs:71-79 - 浏览器 OAuth + PKCE:
run_login_server在本地起 callback server,生成 PKCE pair 与 state,拼auth_url给浏览器跳转,等回调拿到 code 后换 token。codex-rs/login/src/server.rs:151-197 - Device code 流:
run_device_code_login先向/deviceauth/usercode申请 user_code,在终端打印 verification_url 让用户去浏览器输入,然后轮询 token 端点。codex-rs/login/src/device_code_auth.rs:234-238 - CLI 入口包装:
run_login_with_chatgpt/run_login_with_api_key/run_login_with_access_token/run_login_with_device_code在跑前清旧凭据、装文件日志、按forced_login_method校验是否允许该路径,完成后std::process::exit。codex-rs/cli/src/login.rs:167-196 - Auth reload 与撤销:
AuthManager::reload在外部进程刷新 token 后重读auth.json,logout_with_revoke调用 OAuth revoke 端点后清本地凭据。codex-rs/login/src/auth/manager.rs:877-925
设计动机
codex 早期只支持 ChatGPT 浏览器登录和 API key 两条路。ChatGPT 登录走 OAuth + PKCE:本地起 callback server,生成 code_verifier / code_challenge,跳浏览器后 OpenAI 回调 http://localhost:PORT/auth/callback?code=...&state=...,server 用 state 防 CSRF,用 code_verifier 换 token。这套流程必须本机有浏览器,远程 SSH 环境就用不了。
于是加了 device code flow (device_code_auth.rs):用户在终端看到 https://chatgpt.com/device 和一个 user_code,去任意浏览器输入即可。run_login_with_device_code_fallback_to_browser 进一步做 fallback:先尝试 device code,如果 server 返回 404(后端没开 device auth),就退回浏览器 server 流程。
CodexAuth 是个枚举而不是 trait 对象,因为各凭据模式的数据结构差异巨大——API key 就一个字符串,ChatGPT OAuth 有 access_token / refresh_token / id_token 三元组,agent identity 是 JWT。枚举让 auth_mode() 这种匹配一目了然,新增模式也是加变体而不是改 vtable。
AgentIdentityAuthPolicy::JwtOnly vs ChatGptAuth 这两个策略决定"当用户已经用 ChatGPT 登录时,是否自动注册一个 agent identity"。默认 ChatGptAuth 允许这种隐式升级,让 long-running task 不需要单独再登录 agent identity。
关键文件
codex-rs/login/src/auth/manager.rs:71-79 — pub enum CodexAuth,七种凭据模式的根。codex-rs/login/src/auth/manager.rs:929-977 — login_with_access_token,按 token 形态分派成 PAT 或 agent identity JWT。codex-rs/login/src/server.rs:68-148 — ServerOptions / LoginServer / ShutdownHandle,浏览器 OAuth server 的配置与生命周期。codex-rs/login/src/device_code_auth.rs:20-34 — DeviceCode 结构,持有 verification_url、user_code、device_auth_id、polling interval。codex-rs/cli/src/login.rs:52-111 — init_login_file_logging,装一个最小 file-backed tracing layer 写 codex-login.log,故意不复用 TUI 的重 telemetry stack。codex-rs/cli/src/login.rs:119-135 — clear_existing_auth_before_login,登录前先 logout,避免旧 token 残留。codex-rs/tui/src/local_chatgpt_auth.rs:17-59 — load_local_chatgpt_auth,TUI 测试辅助:从本地 auth.json 提取 ChatGPT access_token / account_id / plan_type。CodexAuth 是凭据的唯一真相源,后续 client.rs、provider、telemetry 全从这里 match 出模式。新增一种凭据就是加变体 + 各处加 match arm。
// login/src/auth/manager.rs:71-79 — 七种凭据模式
pub enum CodexAuth {
ApiKey(ApiKeyAuth),
Chatgpt(ChatgptAuth),
ChatgptAuthTokens(ChatgptAuthTokens),
Headers(AuthHeaders),
AgentIdentity(AgentIdentityAuth),
PersonalAccessToken(PersonalAccessTokenAuth),
BedrockApiKey(BedrockApiKeyAuth),
}run_login_with_chatgpt 是 CLI 层的薄包装:加载 config → 装文件日志 → 检查 forced_login_method 是否禁用 ChatGPT 路径 → 清旧凭据 → 起 callback server。注意它返回 !(never),所有出口都是 process::exit,不会让调用方继续往下跑。
// cli/src/login.rs:167-184 — ChatGPT 登录入口,薄包装 + 硬退出
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting browser login flow");
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
match login_with_chatgpt(
config.codex_home.to_path_buf(),
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
config.auth_route_config(),
).await { /* Ok/Err 分别 exit */ }
}login_with_access_token 是"按 token 形态自动分派"的典型——同一个 --with-access-token flag 进来,内部根据 JWT 结构判定走 PAT 路径还是 agent identity JWT 路径,落盘 AuthDotJson 时 auth_mode 字段也对应不同,这样旧的 codex 版本回滚时也能正确反序列化。
// login/src/auth/manager.rs:929-970 — access token 自动分派 PAT vs AgentIdentity
pub async fn login_with_access_token(/* ... */) -> std::io::Result<()> {
let auth_dot_json = match classify_codex_access_token(access_token) {
CodexAccessToken::PersonalAccessToken(access_token) => {
let auth = PersonalAccessTokenAuth::load(access_token, auth_route_config).await?;
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
AuthDotJson { auth_mode: None, personal_access_token: Some(access_token.to_string()), .. }
}
CodexAccessToken::AgentIdentityJwt(jwt) => {
verified_record_from_jwt(jwt, &base_url, auth_route_config).await?;
AuthDotJson { auth_mode: Some(AuthMode::AgentIdentity),
agent_identity: Some(AgentIdentityStorage::Jwt(jwt.to_string())), .. }
}
};
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode, keyring_backend_kind)
}数据流
边界与失败
forced_login_method互斥:管理员可强制只能用 API 或 ChatGPT,CLI 检测到不符时直接 stderr 报错 exit 1,不会等到 OAuth 跑一半被拒。codex-rs/cli/src/login.rs:172-175- device code 不支持时 fallback:
run_login_with_device_code_fallback_to_browser先跑 device code,若返回ErrorKind::NotFound说明后端未开,自动退回浏览器 server 流程;其他错误仍按失败处理。codex-rs/cli/src/login.rs:389-419 auth.json落盘权限:Unix 上OpenOptionsExt::mode(0o600)强制只属主可读,避免凭据被同组用户看到。codex-rs/cli/src/login.rs:69-78- PAT workspace 限制:
ensure_personal_access_token_workspace_allowed在 PAT 登录时校验forced_chatgpt_workspace_id是否匹配,防止 PAT 跨 workspace 误用。codex-rs/login/src/auth/manager.rs:979-985
小结
login/ crate 把五种登录入口统一到 CodexAuth 枚举和 AuthManager 状态机:浏览器 OAuth + PKCE 适合本地、device code 适合远程、API key / PAT / agent identity JWT 适合自动化场景。cli/src/login.rs 是薄包装,负责装文件日志、清旧凭据、按 forced_login_method 守门,所有出口都 process::exit。落盘后 auth.json 是后续所有模型请求的凭据来源,详见 LLM 客户端与 Responses API。登录完成后用户进 TUI 或跑 codex exec,见 CLI 入口与子命令分发。