mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
## Why Enterprise-managed MCP authentication previously required public OAuth clients at both the identity provider and the MCP authorization server, preventing use of registrations that require client secrets. ## What changed - Accept separate `client_secret` and `token_endpoint_auth_method` settings for the enterprise IdP and MCP server OAuth registration. Support `client_secret_basic` and `client_secret_post`, defaulting to Basic when a secret is configured. - Validate the registered method against server metadata and use it for enterprise login and both ID-JAG exchanges. - Require a configured `oauth.authorization_server_issuer` for confidential MCP registrations and reject empty secrets or authentication methods without secrets. - Require the account OAuth client to match the configured IdP client before reusing an account grant with a provisioned secret; otherwise use dedicated enterprise login. ## Testing Extend configuration, login, exchange, and app-server lifecycle tests to cover confidential registrations, separate IdP and resource credentials, authentication method validation, and exclusion of client secrets from authorization URLs, stored login grants, and logs. GitOrigin-RevId: b3be378c2b1bb67e6a3652c648e72fd2e21e956d
116 lines
4.2 KiB
Rust
116 lines
4.2 KiB
Rust
//! Account-grant eligibility and registration-specific sign-in requirements.
|
|
//! The MCP service owns this state across threads; it retains registration policy but no user tokens.
|
|
|
|
use std::sync::Mutex;
|
|
|
|
use codex_config::McpEmaRegistration;
|
|
use codex_config::McpServerIdpOAuthConfig;
|
|
use codex_login::AccountOAuthGrantRevision;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::TokenData;
|
|
use codex_rmcp_client::account_oidc_identity_is_usable;
|
|
use codex_rmcp_client::account_oidc_issuer_hint;
|
|
|
|
/// Remembers a rejected account credential only for the affected EMA registration.
|
|
#[derive(Default)]
|
|
pub struct EmaState {
|
|
rejected: Mutex<Vec<(McpEmaRegistration, AccountOAuthGrantRevision)>>,
|
|
}
|
|
|
|
impl std::fmt::Debug for EmaState {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("EmaState").finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl EmaState {
|
|
pub(crate) fn requires_sign_in(
|
|
&self,
|
|
registration: &McpEmaRegistration,
|
|
tokens: &TokenData,
|
|
) -> bool {
|
|
let revision = AccountOAuthGrantRevision::from(tokens);
|
|
self.rejected
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.iter()
|
|
.any(|(registered, rejected)| registered == registration && rejected == &revision)
|
|
}
|
|
|
|
pub(crate) fn reject(&self, registration: &McpEmaRegistration, tokens: &TokenData) {
|
|
let mut rejected = self
|
|
.rejected
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
// Replacing a registration's marker also bounds repeated refresh/rejection cycles.
|
|
rejected.retain(|(registered, _)| registered != registration);
|
|
rejected.push((
|
|
registration.clone(),
|
|
AccountOAuthGrantRevision::from(tokens),
|
|
));
|
|
}
|
|
|
|
pub(crate) fn account_is_usable(
|
|
&self,
|
|
auth: &CodexAuth,
|
|
registration: &McpEmaRegistration,
|
|
) -> bool {
|
|
matches!(auth, CodexAuth::Chatgpt(_))
|
|
&& auth.get_token_data().is_ok_and(|tokens| {
|
|
account_grant_is_usable(&tokens, registration.idp())
|
|
&& !self.requires_sign_in(registration, &tokens)
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(crate) fn account_grant_is_usable(tokens: &TokenData, idp: &McpServerIdpOAuthConfig) -> bool {
|
|
tokens.oauth_provenance.as_ref().is_some_and(|provenance| {
|
|
provenance.issuer == idp.issuer
|
|
// A provisioned secret belongs to the configured client, not another
|
|
// primary login client at the same IdP. Use dedicated login in that case.
|
|
&& (idp.client_secret.is_none() || provenance.client_id == idp.client_id)
|
|
&& tokens
|
|
.login_owner_id
|
|
.as_ref()
|
|
.is_some_and(|id| !id.is_empty())
|
|
&& !tokens.refresh_token.trim().is_empty()
|
|
&& tokens
|
|
.id_token
|
|
.chatgpt_user_id
|
|
.as_ref()
|
|
.is_some_and(|id| !id.is_empty())
|
|
&& tokens.account_id.as_ref().is_some_and(|id| !id.is_empty())
|
|
&& tokens.id_token.chatgpt_account_id == tokens.account_id
|
|
&& account_oidc_identity_is_usable(
|
|
&tokens.id_token.raw_jwt,
|
|
&provenance.issuer,
|
|
&provenance.client_id,
|
|
)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn account_configuration_issue(
|
|
auth: &CodexAuth,
|
|
idp: &McpServerIdpOAuthConfig,
|
|
) -> Option<&'static str> {
|
|
let tokens = auth.get_token_data().ok()?;
|
|
if tokens.login_owner_id.is_none()
|
|
&& (tokens
|
|
.oauth_provenance
|
|
.as_ref()
|
|
.is_some_and(|provenance| provenance.issuer == idp.issuer)
|
|
|| account_oidc_issuer_hint(&tokens.id_token.raw_jwt, &idp.issuer))
|
|
{
|
|
return Some("Sign in to your Codex account again to enable this enterprise MCP server");
|
|
}
|
|
if tokens.oauth_provenance.as_ref().is_some_and(|provenance| {
|
|
provenance.issuer != idp.issuer
|
|
&& provenance.issuer.trim_end_matches('/') == idp.issuer.trim_end_matches('/')
|
|
}) {
|
|
return Some(
|
|
"Codex sign-in and enterprise MCP issuer strings differ by a trailing slash; align their OIDC issuer configuration",
|
|
);
|
|
}
|
|
None
|
|
}
|