Add staged enterprise OIDC login and coordinated logout (#43844)

## Why

Enterprise sign-in needs to keep browser completion separate from credential storage so callers can recheck the active account and configuration before saving a grant. Logout must also prevent an earlier sign-in from restoring credentials, including from another process sharing `CODEX_HOME`.

## What changed

- Add enterprise login APIs in `rmcp-client` that return an authorization URL and stage validated credentials for an explicit `commit_if` call. Store grants only in the keyring after rechecking caller authority under the credential lock.
- Require a registered client ID, published metadata matching the configured issuer, HTTP loopback callbacks, a refresh token, and a valid OIDC identity assertion. Request `openid` and `offline_access` with `prompt=consent`, and omit MCP resource indicators from authorization and code exchange.
- Persist a login generation under the credential lock so logout invalidates pending and staged sign-ins across processes, even when no grant is stored.
- Keep credentials and account identifiers out of enterprise error chains and logs, and avoid logging callback payloads when the receiver has closed.

## Testing

Add coverage for discovery validation, loopback callbacks, PKCE, staged keyring storage, cancellation, stale attempts, cross-process logout, and error/log privacy. Preserve ordinary MCP OAuth login without a refresh token, and adjust the terminal polling test deadline to include the minimum empty-poll wait.

GitOrigin-RevId: 2a27b9a26505a2f6fdecce8877f6e2c21e148f72
This commit is contained in:
Nick Steele
2026-09-08 16:07:38 +00:00
committed by copyberry
parent cbfa321ecd
commit b090e901f8
13 changed files with 1435 additions and 81 deletions
+2 -1
View File
@@ -951,8 +951,9 @@ async fn stdin_approval_preserves_the_reviewed_terminal() -> anyhow::Result<()>
assert!(original.interaction_lock().try_lock_owned().is_err());
}
// Empty polling must complete without an approval response.
// The test deadline must allow the minimum empty-poll wait.
tokio::time::timeout(
Duration::from_secs(/*secs*/ 5),
Duration::from_millis(MIN_EMPTY_YIELD_TIME_MS) + Duration::from_secs(/*secs*/ 5),
write_stdin(&session, &turn, process_id, "", /*yield_time_ms*/ 250),
)
.await??;