Download codex-rs/app-server/tests/suite/auth.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 19.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server/tests/suite/auth.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/app-server/tests/suite/auth.rs
-
curl -L -o auth.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server/tests/suite/auth.rs
19.2 kB
| use anyhow::Result; | |
| use app_test_support::ChatGptAuthFixture; | |
| use app_test_support::MockResponsesConfig; | |
| use app_test_support::TestAppServer; | |
| use app_test_support::to_response; | |
| use app_test_support::write_chatgpt_auth; | |
| use chrono::Duration; | |
| use chrono::Utc; | |
| use codex_app_server_protocol::Account; | |
| use codex_app_server_protocol::AuthMode; | |
| use codex_app_server_protocol::GetAccountParams; | |
| use codex_app_server_protocol::GetAccountResponse; | |
| use codex_app_server_protocol::GetAuthStatusParams; | |
| use codex_app_server_protocol::GetAuthStatusResponse; | |
| use codex_app_server_protocol::JSONRPCError; | |
| use codex_app_server_protocol::JSONRPCResponse; | |
| use codex_app_server_protocol::LoginAccountResponse; | |
| use codex_app_server_protocol::RequestId; | |
| use codex_config::types::AuthCredentialsStoreMode; | |
| use codex_features::Feature; | |
| use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; | |
| use codex_protocol::account::PlanType as AccountPlanType; | |
| use pretty_assertions::assert_eq; | |
| use std::path::Path; | |
| use tempfile::TempDir; | |
| use tokio::time::timeout; | |
| use wiremock::Mock; | |
| use wiremock::MockServer; | |
| use wiremock::ResponseTemplate; | |
| use wiremock::matchers::header; | |
| use wiremock::matchers::method; | |
| use wiremock::matchers::path; | |
| // Bazel CI can spend tens of seconds starting app-server subprocesses or | |
| // processing auth RPCs under load. | |
| const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); | |
| fn create_config_toml_custom_provider( | |
| codex_home: &Path, | |
| requires_openai_auth: bool, | |
| ) -> std::io::Result<()> { | |
| let mut config = MockResponsesConfig::new("http://127.0.0.1:0") | |
| .with_sandbox_mode("danger-full-access") | |
| .disable_feature(Feature::ShellSnapshot); | |
| if requires_openai_auth { | |
| config = config.with_provider_config("requires_openai_auth = true"); | |
| } | |
| config.write(codex_home) | |
| } | |
| fn create_config_toml(codex_home: &Path) -> std::io::Result<()> { | |
| let config_toml = codex_home.join("config.toml"); | |
| std::fs::write( | |
| config_toml, | |
| r#" | |
| model = "mock-model" | |
| approval_policy = "never" | |
| sandbox_mode = "danger-full-access" | |
| [features] | |
| shell_snapshot = false | |
| "#, | |
| ) | |
| } | |
| fn create_config_toml_forced_login(codex_home: &Path, forced_method: &str) -> std::io::Result<()> { | |
| let config_toml = codex_home.join("config.toml"); | |
| let contents = format!( | |
| r#" | |
| model = "mock-model" | |
| approval_policy = "never" | |
| sandbox_mode = "danger-full-access" | |
| forced_login_method = "{forced_method}" | |
| [features] | |
| shell_snapshot = false | |
| "# | |
| ); | |
| std::fs::write(config_toml, contents) | |
| } | |
| async fn login_with_api_key_via_request(mcp: &mut TestAppServer, api_key: &str) -> Result<()> { | |
| let request_id = mcp.send_login_account_api_key_request(api_key).await?; | |
| let response: LoginAccountResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!(response, LoginAccountResponse::ApiKey {}); | |
| Ok(()) | |
| } | |
| async fn get_auth_status_no_auth() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .with_env_overrides(&[("OPENAI_API_KEY", None)]) | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(false), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!(status.auth_method, None, "expected no auth method"); | |
| assert_eq!(status.auth_token, None, "expected no token"); | |
| Ok(()) | |
| } | |
| async fn get_auth_status_with_api_key() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(false), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!(status.auth_method, Some(AuthMode::ApiKey)); | |
| assert_eq!(status.auth_token, Some("sk-test-key".to_string())); | |
| Ok(()) | |
| } | |
| async fn personal_access_token_without_email_supports_auth_status_and_account_read() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| let server = MockServer::start().await; | |
| Mock::given(method("GET")) | |
| .and(path("/v1/user-auth-credential/whoami")) | |
| .and(header("Authorization", "Bearer at-test-token")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ | |
| "email": null, | |
| "chatgpt_user_id": "user-123", | |
| "chatgpt_account_id": "account-123", | |
| "chatgpt_plan_type": "enterprise_cbp_automation", | |
| "chatgpt_account_is_fedramp": false, | |
| }))) | |
| .expect(1..) | |
| .mount(&server) | |
| .await; | |
| let authapi_base_url = server.uri(); | |
| let mut mcp = TestAppServer::builder() | |
| .with_mock_chatgpt_backend() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .with_env_overrides(&[ | |
| ("OPENAI_API_KEY", None), | |
| ("CODEX_ACCESS_TOKEN", Some("at-test-token")), | |
| ("CODEX_AUTHAPI_BASE_URL", Some(authapi_base_url.as_str())), | |
| ]) | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(false), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!( | |
| status, | |
| GetAuthStatusResponse { | |
| auth_method: Some(AuthMode::PersonalAccessToken), | |
| auth_token: None, | |
| requires_openai_auth: Some(true), | |
| } | |
| ); | |
| let request_id = mcp | |
| .send_get_account_request(GetAccountParams { | |
| refresh_token: false, | |
| }) | |
| .await?; | |
| let response: JSONRPCResponse = timeout( | |
| DEFAULT_READ_TIMEOUT, | |
| mcp.read_stream_until_response_message(RequestId::Integer(request_id)), | |
| ) | |
| .await??; | |
| assert_eq!( | |
| response | |
| .result | |
| .get("account") | |
| .and_then(|account| account.get("email")), | |
| Some(&serde_json::Value::Null), | |
| ); | |
| assert_eq!( | |
| response | |
| .result | |
| .get("account") | |
| .and_then(|account| account.get("planType")) | |
| .and_then(serde_json::Value::as_str), | |
| Some("enterprise_cbp_automation"), | |
| ); | |
| assert_eq!( | |
| to_response::<GetAccountResponse>(response)?, | |
| GetAccountResponse { | |
| workspace_routing: Some(codex_app_server_protocol::WorkspaceRouting { | |
| chatgpt_account_id: "account-123".to_string(), | |
| backend_origin: "https://chatgpt.com".to_string(), | |
| account_routing_override: | |
| codex_app_server_protocol::AccountRoutingOverride::NoConstraint, | |
| }), | |
| account: Some(Account::Chatgpt { | |
| email: None, | |
| plan_type: AccountPlanType::EnterpriseCbpAutomation, | |
| }), | |
| requires_openai_auth: true, | |
| } | |
| ); | |
| server.verify().await; | |
| Ok(()) | |
| } | |
| async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml_custom_provider(codex_home.path(), /*requires_openai_auth*/ false)?; | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(false), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!(status.auth_method, None, "expected no auth method"); | |
| assert_eq!(status.auth_token, None, "expected no token"); | |
| assert_eq!( | |
| status.requires_openai_auth, | |
| Some(false), | |
| "requires_openai_auth should be false", | |
| ); | |
| Ok(()) | |
| } | |
| async fn get_auth_status_with_api_key_no_include_token() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; | |
| // Build params via struct so None field is omitted in wire JSON. | |
| let params = GetAuthStatusParams { | |
| include_token: None, | |
| refresh_token: Some(false), | |
| }; | |
| let request_id = mcp.send_get_auth_status_request(params).await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!(status.auth_method, Some(AuthMode::ApiKey)); | |
| assert!(status.auth_token.is_none(), "token must be omitted"); | |
| Ok(()) | |
| } | |
| async fn get_auth_status_with_api_key_refresh_requested() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(true), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!( | |
| status, | |
| GetAuthStatusResponse { | |
| auth_method: Some(AuthMode::ApiKey), | |
| auth_token: Some("sk-test-key".to_string()), | |
| requires_openai_auth: Some(true), | |
| } | |
| ); | |
| Ok(()) | |
| } | |
| async fn get_auth_status_omits_token_after_permanent_refresh_failure() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| write_chatgpt_auth( | |
| codex_home.path(), | |
| ChatGptAuthFixture::new("stale-access-token") | |
| .refresh_token("stale-refresh-token") | |
| .account_id("acct_123") | |
| .email("user@example.com") | |
| .plan_type("pro"), | |
| AuthCredentialsStoreMode::File, | |
| )?; | |
| let server = MockServer::start().await; | |
| Mock::given(method("POST")) | |
| .and(path("/oauth/token")) | |
| .respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({ | |
| "error": { | |
| "code": "refresh_token_reused" | |
| } | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let refresh_url = format!("{}/oauth/token", server.uri()); | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .with_env_overrides(&[ | |
| ("OPENAI_API_KEY", None), | |
| ( | |
| REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR, | |
| Some(refresh_url.as_str()), | |
| ), | |
| ]) | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(true), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!( | |
| status, | |
| GetAuthStatusResponse { | |
| auth_method: Some(AuthMode::Chatgpt), | |
| auth_token: None, | |
| requires_openai_auth: Some(true), | |
| } | |
| ); | |
| let second_request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(true), | |
| }) | |
| .await?; | |
| let second_status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(second_request_id)).await??; | |
| assert_eq!(second_status, status); | |
| server.verify().await; | |
| Ok(()) | |
| } | |
| async fn get_auth_status_omits_token_after_proactive_refresh_failure() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| write_chatgpt_auth( | |
| codex_home.path(), | |
| ChatGptAuthFixture::new("stale-access-token") | |
| .refresh_token("stale-refresh-token") | |
| .account_id("acct_123") | |
| .email("user@example.com") | |
| .plan_type("pro") | |
| .last_refresh(Some(Utc::now() - Duration::days(9))), | |
| AuthCredentialsStoreMode::File, | |
| )?; | |
| let server = MockServer::start().await; | |
| Mock::given(method("POST")) | |
| .and(path("/oauth/token")) | |
| .respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({ | |
| "error": { | |
| "code": "refresh_token_reused" | |
| } | |
| }))) | |
| .expect(2) | |
| .mount(&server) | |
| .await; | |
| let refresh_url = format!("{}/oauth/token", server.uri()); | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .with_env_overrides(&[ | |
| ("OPENAI_API_KEY", None), | |
| ( | |
| REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR, | |
| Some(refresh_url.as_str()), | |
| ), | |
| ]) | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| let request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(false), | |
| }) | |
| .await?; | |
| let status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; | |
| assert_eq!( | |
| status, | |
| GetAuthStatusResponse { | |
| auth_method: Some(AuthMode::Chatgpt), | |
| auth_token: None, | |
| requires_openai_auth: Some(true), | |
| } | |
| ); | |
| server.verify().await; | |
| Ok(()) | |
| } | |
| async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml(codex_home.path())?; | |
| write_chatgpt_auth( | |
| codex_home.path(), | |
| ChatGptAuthFixture::new("stale-access-token") | |
| .refresh_token("stale-refresh-token") | |
| .account_id("acct_123") | |
| .email("user@example.com") | |
| .plan_type("pro") | |
| .last_refresh(Some(Utc::now() - Duration::days(9))), | |
| AuthCredentialsStoreMode::File, | |
| )?; | |
| let server = MockServer::start().await; | |
| Mock::given(method("POST")) | |
| .and(path("/oauth/token")) | |
| .respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({ | |
| "error": { | |
| "code": "refresh_token_reused" | |
| } | |
| }))) | |
| .expect(2) | |
| .mount(&server) | |
| .await; | |
| let refresh_url = format!("{}/oauth/token", server.uri()); | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .with_env_overrides(&[ | |
| ("OPENAI_API_KEY", None), | |
| ( | |
| REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR, | |
| Some(refresh_url.as_str()), | |
| ), | |
| ]) | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| let failed_request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(true), | |
| }) | |
| .await?; | |
| let failed_status: GetAuthStatusResponse = | |
| timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(failed_request_id)).await??; | |
| assert_eq!( | |
| failed_status, | |
| GetAuthStatusResponse { | |
| auth_method: Some(AuthMode::Chatgpt), | |
| auth_token: None, | |
| requires_openai_auth: Some(true), | |
| } | |
| ); | |
| write_chatgpt_auth( | |
| codex_home.path(), | |
| ChatGptAuthFixture::new("recovered-access-token") | |
| .refresh_token("recovered-refresh-token") | |
| .account_id("acct_123") | |
| .email("user@example.com") | |
| .plan_type("pro") | |
| .last_refresh(Some(Utc::now())), | |
| AuthCredentialsStoreMode::File, | |
| )?; | |
| let recovered_request_id = mcp | |
| .send_get_auth_status_request(GetAuthStatusParams { | |
| include_token: Some(true), | |
| refresh_token: Some(false), | |
| }) | |
| .await?; | |
| let recovered_status: GetAuthStatusResponse = timeout( | |
| DEFAULT_READ_TIMEOUT, | |
| mcp.read_response(recovered_request_id), | |
| ) | |
| .await??; | |
| assert_eq!( | |
| recovered_status, | |
| GetAuthStatusResponse { | |
| auth_method: Some(AuthMode::Chatgpt), | |
| auth_token: Some("recovered-access-token".to_string()), | |
| requires_openai_auth: Some(true), | |
| } | |
| ); | |
| server.verify().await; | |
| Ok(()) | |
| } | |
| async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()> { | |
| let codex_home = TempDir::new()?; | |
| create_config_toml_forced_login(codex_home.path(), "chatgpt")?; | |
| let mut mcp = TestAppServer::builder() | |
| .with_codex_home(codex_home.path()) | |
| .without_auto_env() | |
| .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) | |
| .await?; | |
| let request_id = mcp | |
| .send_login_account_api_key_request("sk-test-key") | |
| .await?; | |
| let err: JSONRPCError = timeout( | |
| DEFAULT_READ_TIMEOUT, | |
| mcp.read_stream_until_error_message(RequestId::Integer(request_id)), | |
| ) | |
| .await??; | |
| assert_eq!( | |
| err.error.message, | |
| "API key login is disabled. Use ChatGPT login instead." | |
| ); | |
| Ok(()) | |
| } | |