Skip to content

auth: let AuthManager own external bearer auth#16287

Merged
bolinfest merged 1 commit intomainfrom
pr16287
Mar 31, 2026
Merged

auth: let AuthManager own external bearer auth#16287
bolinfest merged 1 commit intomainfrom
pr16287

Conversation

@bolinfest
Copy link
Copy Markdown
Collaborator

@bolinfest bolinfest commented Mar 31, 2026

Summary

AuthManager and UnauthorizedRecovery already own token resolution and staged 401 recovery. The missing piece for provider auth was a bearer-only mode that still fit that design, instead of pushing a second auth abstraction into codex-core.

This PR keeps the design centered on AuthManager: it teaches codex-login how to own external bearer auth directly so later provider work can keep calling AuthManager.auth() and UnauthorizedRecovery.

Motivation

This is the middle layer for #15189.

The intended design is still:

  • AuthManager encapsulates token storage and refresh
  • UnauthorizedRecovery powers staged 401 recovery
  • all request tokens go through AuthManager.auth()

This PR makes that possible for provider-backed bearer tokens by adding a bearer-only auth mode inside AuthManager instead of building parallel request-auth plumbing in core.

What Changed

  • move ModelProviderAuthInfo into codex-protocol so core and login share one config shape
  • add login/src/auth/external_bearer.rs, which runs the configured command, caches the bearer token in memory, and refreshes it after 401
  • add AuthManager::external_bearer_only(...) for provider-scoped request paths that should use command-backed bearer auth without mutating the shared OpenAI auth manager
  • add AuthManager::shared_with_external_chatgpt_auth_refresher(...) and rename the other AuthManager helpers that only apply to external ChatGPT auth so the ChatGPT-only path is explicit at the call site
  • keep external ChatGPT refresh behavior unchanged while ensuring bearer-only external auth never persists to auth.json

Testing

  • cargo test -p codex-login
  • cargo test -p codex-protocol

Stack created with Sapling. Best reviewed with ReviewStack.

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4127419694

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 1211 to 1215
if let Some(auth) = self.resolve_external_api_key_auth().await {
return Some(auth);
}

let auth = self.auth_cached()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid falling back to shared auth when bearer resolve fails

When a manager is created via with_external_bearer_refresher, auth() still falls back to auth_cached() if resolve_external_api_key_auth() returns None (including resolver errors). Because the derived manager shares inner with the base manager, this can silently attach the base OpenAI credential to requests that were supposed to use external provider bearer auth, which is both a security leak and a behavior regression whenever provider token resolution is unavailable.

Useful? React with 👍 / 👎.

Comment on lines +1585 to +1587
let refreshed = handle.refresher.refresh(context).await?;
if handle.kind == ExternalAuthKind::Bearer {
return Ok(());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply refreshed bearer token during unauthorized recovery

In the bearer path, refresh_external_auth() calls refresh() but then immediately returns without using refreshed.access_token. For refresher implementations where resolve() does not instantly reflect the refresh side effect, the retry after a reported successful recovery will continue using the stale token and keep failing with 401. The refreshed token should be made visible to subsequent auth() calls in this manager.

Useful? React with 👍 / 👎.

@bolinfest bolinfest force-pushed the pr16287 branch 4 times, most recently from 346440c to 4a42882 Compare March 31, 2026 08:01
bolinfest added a commit that referenced this pull request Mar 31, 2026
## Summary

`ExternalAuthRefresher` was still shaped around external ChatGPT auth:
`ExternalAuthTokens` always implied ChatGPT account metadata even when a
caller only needed a bearer token.

This PR generalizes that contract so bearer-only sources are
first-class, while keeping the existing ChatGPT paths strict anywhere we
persist or rebuild ChatGPT auth state.

## Motivation

This is the first step toward #15189.

The follow-on provider-auth work needs one shared external-auth contract
that can do both of these things:

- resolve the current bearer token before a request is sent
- return a refreshed bearer token after a `401`

That should not require a second token result type just because there is
no ChatGPT account metadata attached.

## What Changed

- change `ExternalAuthTokens` to carry `access_token` plus optional
`ExternalAuthChatgptMetadata`
- add helper constructors for bearer-only tokens and ChatGPT-backed
tokens
- add `ExternalAuthRefresher::resolve()` with a default no-op
implementation so refreshers can optionally provide the current token
before a request is sent
- keep ChatGPT-only persistence strict by continuing to require ChatGPT
metadata anywhere the login layer seeds or reloads ChatGPT auth state
- update the app-server bridge to construct the new token shape for
external ChatGPT auth refreshes

## Testing

- `cargo test -p codex-login`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16286).
* #16288
* #16287
* __->__ #16286
Base automatically changed from pr16286 to main March 31, 2026 08:02
@bolinfest bolinfest merged commit 0071968 into main Mar 31, 2026
36 checks passed
@bolinfest bolinfest deleted the pr16287 branch March 31, 2026 08:26
@github-actions github-actions bot locked and limited conversation to collaborators Mar 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant