登录与认证 · ChatGPT、API key 与网关

认证集中在 codex-login:八种凭据统一成 CodexAuth,由 AuthManager 按固定优先级加载、缓存、刷新。本篇讲浏览器登录的本地回调与 PKCE、设备码怎么复用同一套换码流程、多进程共用 auth.json 时怎样安全刷新令牌、凭据存在文件还是系统钥匙串,以及 Agent Identity、workload identity 与模型网关 OAuth 这几种面向企业的认证。

作者 David更新于 第 50 篇(共 57 篇)

登录与认证 · ChatGPT、API key 与网关

Codex 发给模型的每个请求都要带凭据,app-server 连远端、exec-server 注册执行环境也要。这些都出自同一个 crate:codex-login。它把形形色色的凭据统一成一个枚举,再由一个管理器负责加载、缓存、刷新与登出,其余模块只管问“现在用什么凭据”。

怎么用

codex login 打开浏览器走 ChatGPT 登录,--device-auth 用设备码,--with-api-key、--with-access-token 从 stdin 读 API key 或访问令牌,codex login status 看状态,codex logout 登出。凭据默认存在 $CODEX_HOME/auth.json,config.toml 的 cli_auth_credentials_store 可以改成 keyring 或 auto;托管配置可用 forced_login_method、forced_chatgpt_workspace_id 限定登录方式与工作区。完整说明见手册的安装、登录与升级。

CodexAuth:八种凭据,一个入口

所有凭据都归结为这个枚举:

/// Authentication mechanism used by the current user.
#[derive(Debug, Clone)]
pub enum CodexAuth {
    ApiKey(ApiKeyAuth),
    Chatgpt(ChatgptAuth),
    ChatgptAuthTokens(ChatgptAuthTokens),
    Headers(AuthHeaders),
    AgentIdentity(AgentIdentityAuth),
    PersonalAccessToken(PersonalAccessTokenAuth),
    BedrockApiKey(BedrockApiKeyAuth),
    BedrockAccessKeys(BedrockAccessKeysAuth),
}

(codex-rs/login/src/auth/manager.rs:90)

Chatgpt 是 Codex 自己登录、自己刷新的 ChatGPT 令牌;ChatgptAuthTokens 是宿主应用经 app-server 的 account/login/start 注入的令牌,只放在进程内存里,刷新也由宿主负责;ApiKey 是 OpenAI API key;PersonalAccessToken 与 AgentIdentity 来自环境变量 CODEX_ACCESS_TOKEN;两个 Bedrock 变体服务 Amazon Bedrock。

AuthManager 是唯一的事实来源。注释说得直白:它加载一次后发出 CodexAuth 的克隆,外部改了 auth.json 也要等显式 reload() 才看得见,免得同一次运行里不同模块看到不一致的凭据。加载顺序写在 load_auth(codex-rs/login/src/auth/manager.rs:1488):

图表加载中…

codex exec、codex doctor、codex exec-server --remote 等调用方会读 CODEX_API_KEY,交互式 TUI 不读;OPENAI_API_KEY 不在这条链上,TUI 只拿它来预填 API key 登录框。另有一个 ExternalAuth trait,装上之后 AuthManager 不再读上面四个来源,改为向它要凭据、要刷新。仓库里有三个实现:app-server 的 ExternalAuthBridge(宿主注入令牌,刷新时反向发 account/chatgptAuthTokens/refresh 请求,等 10 秒)、按模型提供方配置执行命令取令牌的 BearerTokenRefresher,以及后面要讲的 workload identity。

凭据最终由 codex-model-provider 的 AuthProvider 变成请求头:API key、ChatGPT 令牌与 PAT 走 Authorization: Bearer,有账号时加 ChatGPT-Account-ID,FedRAMP 账号再加 X-OpenAI-Fedramp;Agent Identity 则每次请求现签一个 AgentAssertion。

浏览器登录:本地回调与 PKCE

codex login 先吊销并清掉旧凭据(clear_existing_auth_before_login),再起一个只监听 127.0.0.1 的小 HTTP 服务等 OAuth 回调:

图表加载中…

PKCE 的两个值这样生成,verifier 只留在本机,授权 URL 里只放它的哈希:

pub(crate) fn generate_pkce() -> PkceCodes {
    let mut bytes = [0u8; 64];
    rand::rng().fill_bytes(&mut bytes);

    // Verifier: URL-safe base64 without padding (43..128 chars)
    let code_verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);

    // Challenge (S256): BASE64URL-ENCODE(SHA256(verifier)) without padding
    let digest = Sha256::digest(code_verifier.as_bytes());
    let code_challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);

    PkceCodes {
        code_verifier,
        code_challenge,
    }
}

(codex-rs/login/src/oauth/pkce.rs:14)

几处细节:授权请求的 scope 是 openid profile email offline_access api.connectors.read api.connectors.invoke,并带上 originator 与限定工作区用的 allowed_workspace_id;回调必须先过 state 校验,才去看 code 或错误信息(CallbackParameters::validate);端口被占用时,多半是上一次没结束的登录服务,于是先向它发 GET /cancel,每 200 毫秒重试、最多 10 次,仍不行才换到 1457——注释说这个备用端口要和服务端登记的回调地址白名单保持一致。模块开头的注释还专门强调:返回给用户的错误可以详细,写进日志的只能是审核过的字段和脱敏后的 URL。

设备码:给没有浏览器的机器

--device-auth 先向 {issuer}/api/accounts/deviceauth/usercode 申请用户码,提示用户打开 {issuer}/codex/device 输入,然后按服务端给的间隔轮询 deviceauth/token:返回 403 或 404 表示还没授权,继续等,最长 15 分钟。设计上的巧处在于轮询成功时,服务端回的是授权码加上一对 PKCE 值,客户端接着走和浏览器登录完全相同的 exchange_code_for_tokens,只是回调地址换成 {issuer}/deviceauth/callback(codex-rs/login/src/device_code_auth.rs:182)。两条路径共用换码、工作区校验与持久化代码,差别只在“授权码从哪来”。服务端没开设备码时申请用户码返回 404,Codex 会报出“device code login is not enabled”。

令牌刷新:主动、被动与多进程

ChatGPT 登录拿到的是短期 access token 加长期 refresh token,刷新分两种时机:

  • 主动:每次 AuthManager::auth() 取凭据前检查,access token 这个 JWT 离过期不到 5 分钟就先刷新;解析不出过期时间时,退而看上次刷新是否已超过 8 天(should_refresh_proactively)。
  • 被动:请求撞上 401 时,UnauthorizedRecovery 状态机依次尝试:先从存储重新加载(只在账号 ID 没变时),再用 refresh token 刷新,都不行才把错误交给用户;API key、PAT 等不做恢复。外部凭据则只重试一次,请宿主刷新。

难点在多进程。同一台机器上可能同时开着好几个 Codex,共用一个 auth.json;refresh token 用过一次就作废,两个进程各刷一遍,后刷的那个必然失败。refresh_token 因此先做一次“有保护的重新加载”:

    /// Attempt to refresh the token by first performing a guarded reload from
    /// the active auth source. If the loaded token differs from the cached token,
    /// we can assume that the source already refreshed it. Otherwise, ask the
    /// token authority to refresh.
    pub async fn refresh_token(&self) -> Result<(), RefreshTokenError> {
        // ...
        let expected_account_id = auth_before_reload
            .as_ref()
            .and_then(CodexAuth::get_account_id);

        match self
            .reload_if_account_id_matches(expected_account_id.as_deref())
            .await
        {
            ReloadOutcome::ReloadedChanged => {
                tracing::info!("Skipping token refresh because auth changed after guarded reload.");
                Ok(())
            }
            ReloadOutcome::ReloadedNoChange => self.refresh_token_from_authority_impl().await,
            ReloadOutcome::Skipped => {
                // ...
            }
        }
    }

(codex-rs/login/src/auth/manager.rs:2844)

磁盘上的令牌已经变了,说明别的进程刚刷过,直接用新的;账号 ID 对不上(用户在别处换了账号),则拒绝刷新并提示重新登录。进程内还有一把信号量(refresh_lock)保证同一时刻只有一个刷新在跑。刷新请求打到 https://auth.openai.com/oauth/token,服务端返回 refresh_token_expired、refresh_token_reused、refresh_token_invalidated 时归为永久失败,给出对应的“请重新登录”提示,并记在这份凭据名下不再重试;网络错误之类归为暂时失败。登出时 logout_with_revoke 先尽力向 /oauth/revoke 吊销 refresh token(没有就吊销 access token),失败也照样删本地凭据。

凭据存在哪里

存储抽象成只有 load、save、delete 三个方法的 AuthStorageBackend,按配置挑实现:

fn create_auth_storage_with_store(
    codex_home: PathBuf,
    mode: AuthCredentialsStoreMode,
    keyring_store: Arc<dyn KeyringStore>,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Arc<dyn AuthStorageBackend> {
    match mode {
        AuthCredentialsStoreMode::File => Arc::new(FileAuthStorage::new(codex_home)),
        AuthCredentialsStoreMode::Keyring => {
            create_keyring_auth_storage(codex_home, keyring_store, keyring_backend_kind)
        }
        AuthCredentialsStoreMode::Auto => Arc::new(AutoAuthStorage::new(
            codex_home,
            keyring_store,
            keyring_backend_kind,
        )),
        AuthCredentialsStoreMode::Ephemeral => Arc::new(EphemeralAuthStorage::new(codex_home)),
    }
}

(codex-rs/login/src/auth/storage.rs:511)

  • File(默认):$CODEX_HOME/auth.json,Unix 上以 0o600 权限创建。
  • Keyring:钥匙串里服务名是 Codex Auth,账户名是 cli| 加 CODEX_HOME 规范路径 SHA-256 的前 16 位十六进制,不同的 CODEX_HOME 互不干扰。又分两种后端:Direct 把整份 JSON 直接存进钥匙串;Secrets 把它存进本地加密的 secrets 文件,钥匙串只保管文件密钥。后者由 secret_auth_storage 特性控制,默认只在 Windows 上打开。写进钥匙串成功后会顺手删掉残留的 auth.json。
  • Auto:先用钥匙串,读写失败就退回文件,只记一条警告。
  • Ephemeral:进程内的全局表,按同样的键区分 CODEX_HOME,宿主注入的令牌就放在这里,进程退出即消失。

钥匙串访问由 codex-keyring-store 封装:它在 keyring crate 之上定义 KeyringStore trait(测试时换成 MockKeyringStore),并按平台打开不同的后端特性——macOS 是 apple-native,Windows 是 windows-native,Linux 是 linux-native-async-persistent,FreeBSD 与 OpenBSD 是 sync-secret-service。

企业与自动化:Agent Identity、workload identity 与网关

面向组织的几种认证走的是另一套思路:不存用户令牌,而是用机器身份。

  • Agent Identity:CODEX_ACCESS_TOKEN 若是 Agent Identity JWT,先按 JWKS 验签,得到 agent 运行时 ID 与私钥;也可以由已登录的 ChatGPT 账号现场注册一个。每次启动再向服务端登记一个 task,之后每个请求带 AgentAssertion 头,内容是用 Ed25519 私钥对“运行时 ID、task ID、时间戳”的签名(codex-rs/agent-identity/src/lib.rs:232)。由 ChatGPT 账号现场注册时,遇到 429、5xx 或网络错误最多试 3 次,仍失败则同一账号冷却 1 小时再试。
  • workload identity:设了 OPENAI_FEDERATION_RULE_ID 或 OPENAI_IDENTITY_TOKEN_FILE 任意一个即启用,缺项直接报错而不是退回别的凭据。它把 OPENAI_IDENTITY_TOKEN_FILE 指向的文件里的身份断言换成 ChatGPT 凭据,作为 ExternalAuth 装进 AuthManager;codex logout 对它无效,报错说这种凭据由宿主管理。
  • 网关 OAuth:有些模型提供方前面还挡着一层企业网关。[model_providers.<id>.gateway_oauth] 配好授权与令牌端点、client_id、scope 和回调端口后,GatewayAuthManager 为这层网关单独走一遍 PKCE 授权,把令牌以指定的请求头或 Cookie 附在请求上,与提供方自己的认证并存。令牌存进加密 secrets 的独立命名空间,离过期不到 30 秒就刷新;多份配置共用一个加密文件,所以换码期间用 secrets/gateway_oauth.lock 文件锁串行。app-server 用 account/gatewayOAuth/read、login、cancel 三个方法把登录交给前端。

和《从 LLM 到 Coding Agent》对照

从裸调 API 开始那篇只需要一个 API key。落到一个多端共用、长期运行的产品里,认证就成了一个独立子系统:令牌会过期要刷新,refresh token 只能用一次所以多进程要协调,凭据要进系统钥匙串,出了 401 要有恢复步骤,企业还要能锁定登录方式与工作区。Codex 用一个枚举加一个管理器把这些复杂度收拢,模型客户端、远程控制、exec-server 注册都只跟 AuthManager 打交道。


上一篇:exec-server · 在另一台机器上执行 · 下一篇:实时语音 · 语音对话的实现

本页目录