Files
codex/codex-rs/model-provider/src/shared_state_test_support.rs
T
smurching-oai df38cb3b2a Add device-code authorization for gateway OAuth (#59822)
## Why

Gateway sign-in needs an alternative for terminals that cannot open a browser.

## What changed

- Add optional `gateway_oauth.device_authorization_url` configuration and a `login_with_device_code` API that presents a verification URL and user code, polls for approval, and persists the resulting credentials.
- Use the configured proxy and network policy, validate verification URLs, and bound OAuth responses. Support cancellation and report denied or expired codes without exposing issuer error details.

## Testing

Add tests for successful authorization and credential persistence, unsafe verification URLs, network-policy denials, denied and expired codes, and cancellation during approval or credential storage.

GitOrigin-RevId: 96398578fb866d34929e85c04eadf468d8cd3255
2026-11-06 04:24:04 +00:00

140 lines
4.9 KiB
Rust

//! Seeds gateway credentials in the configured store for provider and agent integration tests.
#![allow(clippy::expect_used)] // Fixture setup failures should fail the calling test immediately.
use std::sync::Arc;
use std::sync::Mutex;
use codex_keyring_store::CredentialStoreError;
use codex_keyring_store::KeyringStore;
use codex_login::AuthManager;
use codex_login::GatewayAuthConfig;
use codex_login::OAuthSessionManager;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::OAuthCredentialsStore;
use codex_secrets::LocalSecretsNamespace;
use codex_secrets::SecretName;
use codex_secrets::SecretScope;
use codex_secrets::SecretsBackendKind;
use codex_secrets::SecretsManager;
use super::process_shared_state;
#[derive(Debug)]
struct TestKeyring(Mutex<Option<String>>);
impl KeyringStore for TestKeyring {
fn load(
&self,
_service: &str,
_account: &str,
) -> std::result::Result<Option<String>, CredentialStoreError> {
Ok(self
.0
.lock()
.expect("gateway test token store lock")
.clone())
}
fn save(
&self,
_service: &str,
_account: &str,
value: &str,
) -> std::result::Result<(), CredentialStoreError> {
*self.0.lock().expect("gateway test token store lock") = Some(value.to_string());
Ok(())
}
fn delete(
&self,
_service: &str,
_account: &str,
) -> std::result::Result<bool, CredentialStoreError> {
Ok(self
.0
.lock()
.expect("gateway test token store lock")
.take()
.is_some())
}
}
/// Seeds a provider's configured gateway store, using a test passphrase for the keyring backend.
/// Keep the returned manager alive while requests use this fixture.
pub fn seed_gateway_auth(
info: &ModelProviderInfo,
primary: &AuthManager,
token: serde_json::Value,
) -> Arc<OAuthSessionManager> {
let Some(codex_model_provider_info::GatewayOAuthConfig::OAuth(config)) =
info.gateway_oauth.clone()
else {
panic!("expected gateway OAuth test configuration")
};
let runtime = primary.runtime_config();
let oauth = GatewayAuthConfig {
authorization_url: config.authorization_url,
device_authorization_url: config.device_authorization_url,
token_url: config.token_url,
client_id: config.client_id,
resource: config.resource,
scopes: config.scopes,
redirect_port: config.redirect_port,
credentials_store: config.credentials_store,
};
// Repeated fixtures sharing a Codex home must use the same encryption key.
let keyring = Arc::new(TestKeyring(Mutex::new(Some(
"gateway-test-passphrase".to_string(),
))));
let manager = Arc::new(
OAuthSessionManager::new(
oauth.clone(),
runtime.codex_home.clone(),
runtime.auth_route_config.http_client_factory(),
keyring.clone(),
)
.expect("gateway test HTTP client"),
);
let credential_id = manager.credential_id();
match oauth.credentials_store {
OAuthCredentialsStore::Keyring => {
let secrets = SecretsManager::new_with_keyring_store_and_namespace(
runtime.codex_home.clone(),
SecretsBackendKind::Local,
keyring,
LocalSecretsNamespace::GatewayOAuth,
);
let digest = credential_id
.strip_prefix("provider-oauth|")
.expect("gateway credential ID");
let name = SecretName::new(&format!("PROVIDER_OAUTH_{}", digest.to_ascii_uppercase()))
.expect("gateway test credential name");
secrets
.set(&SecretScope::Global, &name, &token.to_string())
.expect("seed encrypted gateway credentials");
}
OAuthCredentialsStore::File => {
let dir = runtime.codex_home.join("secrets");
std::fs::create_dir_all(&dir).expect("create test credentials directory");
let file = dir.join("gateway_oauth.json");
let mut tokens: std::collections::BTreeMap<String, serde_json::Value> =
match std::fs::read(&file) {
Ok(bytes) => serde_json::from_slice(&bytes).expect("parse test gateway tokens"),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Default::default(),
Err(err) => panic!("read test gateway file: {err}"),
};
tokens.insert(credential_id, token);
std::fs::write(
file,
serde_json::to_vec(&tokens).expect("serialize test gateway tokens"),
)
.expect("seed test gateway file");
}
}
process_shared_state()
.oauth_sessions
.lock()
.expect("gateway test manager registry lock")
.push((oauth, runtime, Arc::downgrade(&manager)));
manager
}