Skip to content

feat: verify agent identity JWTs with JWKS#19764

Merged
efrazer-oai merged 4 commits into
mainfrom
dev/efrazer/agent-identity-jwt-verify
Apr 28, 2026
Merged

feat: verify agent identity JWTs with JWKS#19764
efrazer-oai merged 4 commits into
mainfrom
dev/efrazer/agent-identity-jwt-verify

Conversation

@efrazer-oai
Copy link
Copy Markdown
Contributor

@efrazer-oai efrazer-oai commented Apr 27, 2026

Summary

AgentIdentity auth could previously read the JWT payload but did not verify that the token was signed by the backend. This PR verifies AgentIdentity JWTs by fetching the backend JWKS and passing the matching key into the existing jsonwebtoken verification path.

The existing login/storage write path now verifies the token before saving it. The auth load path fetches JWKS from the configured ChatGPT backend URL, verifies issuer/audience/signature/expiry, then constructs the same AgentIdentity auth record from the verified claims. Task registration remains separate and uses the agent_identity_authapi_base_url config introduced in the prior PR.

Stack

  1. refactor: make auth loading async (merged)
  2. refactor: load AgentIdentity runtime eagerly
  3. fix: configure AgentIdentity AuthAPI base URL
  4. This PR: feat: verify AgentIdentity JWTs with JWKS

Important call sites

Area Change
codex-agent-identity Adds JWKS fetch, path-style URL construction, and jsonwebtoken verification using kid.
codex-login auth load AgentIdentity JWT auth now fetches JWKS before constructing AgentIdentityAuth.
codex login status Passes the configured ChatGPT base URL so status uses the same JWKS endpoint as the running app.
AgentIdentity env-auth test Uses a signed JWT and mocked JWKS endpoint, then verifies task registration still happens through the configured AuthAPI base URL.

Design decisions

JWT verification stays in codex-agent-identity, next to the claim type and existing JWT parsing helper. The implementation delegates signature and claim validation to jsonwebtoken; this code only fetches the JWKS, selects the trusted key by kid, and supplies issuer/audience requirements.

The JWKS URL follows existing Codex backend path style: /backend-api bases use /wham/agent-identities/jwks, while direct Codex API bases use /api/codex/agent-identities/jwks.

chatgpt_base_url is only for fetching the JWKS/public-key material. AuthAPI task registration uses the separate agent_identity_authapi_base_url config, with production AuthAPI as the default when no override is configured.

Testing

Tests: targeted AgentIdentity tests, targeted AgentIdentity env-auth test, targeted Rust checks for touched crates, formatter, scoped Clippy fix, and Bazel lock check.

@efrazer-oai
Copy link
Copy Markdown
Contributor Author

@codex review

@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-eager-runtime branch 2 times, most recently from ee7d9b3 to d69ac63 Compare April 27, 2026 04:55
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

if let Some(stripped) = base_url.strip_suffix("/codex") {
base_url = stripped.to_string();

P1 Badge Preserve /api/codex when normalizing base URL

normalize_chatgpt_base_url removes any trailing /codex. With a configured base like http://host/api/codex, this truncates to http://host/api; then JWKS URL construction becomes .../api/api/codex/agent-identities/jwks. That breaks JWKS fetch and causes valid AgentIdentity JWTs to fail verification on direct Codex API deployments.

ℹ️ 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 thread codex-rs/login/src/auth/manager.rs Outdated
/*enable_codex_api_key_env*/ true,
config.auth_credentials_store_mode,
)?
/*chatgpt_base_url*/ None,
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 Use configured base URL in login restriction auth load

enforce_login_restrictions always calls load_auth(..., None) for chatgpt_base_url. After this change, AgentIdentity auth verification fetches JWKS using that value, so this path falls back to https://chatgpt.com/backend-api instead of the runtime-configured backend. In non-default/staging/self-hosted setups, restriction checks can fail and abort startup even with valid credentials.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added this, also simplified he normalize_base_url thing it shouldn't really have existed in the way it did.

Comment thread codex-rs/agent-identity/src/lib.rs
Comment thread codex-rs/agent-identity/src/lib.rs
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-eager-runtime branch 2 times, most recently from 64a404b to c21a966 Compare April 27, 2026 05:09
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch 2 times, most recently from 4a656a8 to 8c73abd Compare April 27, 2026 05:47
@efrazer-oai
Copy link
Copy Markdown
Contributor Author

@codex review

@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from 8c73abd to 464097e Compare April 27, 2026 05:50
@efrazer-oai
Copy link
Copy Markdown
Contributor Author

@codex review

@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from 464097e to f085622 Compare April 27, 2026 05:53
@efrazer-oai
Copy link
Copy Markdown
Contributor Author

@codex review

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: f085622f31

ℹ️ 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 thread codex-rs/agent-identity/src/lib.rs Outdated
Comment on lines 348 to 351
{
base_url = stripped.to_string();
}
if (base_url.starts_with("https://chatgpt.com")
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 Preserve Codex API base URLs on ChatGPT hosts

Do not append /backend-api when the configured base URL already targets /api/codex. In normalize_chatgpt_base_url, a value like https://chatgpt.com/api/codex is rewritten to .../api/codex/backend-api, which makes agent_identity_jwks_url build a WHAM JWKS path that does not match Codex-API style routing and causes JWT verification to fail.

Useful? React with 👍 / 👎.

@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from f085622 to ec92ff1 Compare April 27, 2026 06:21
@efrazer-oai
Copy link
Copy Markdown
Contributor Author

@codex review

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: ec92ff1eae

ℹ️ 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".

let decoding_key = DecodingKey::from_jwk(jwk).context("failed to build JWT decoding key")?;
let mut validation = Validation::new(Algorithm::RS256);
validation.set_audience(&[AGENT_IDENTITY_JWT_AUDIENCE]);
validation.set_issuer(&[AGENT_IDENTITY_JWT_ISSUER]);
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 Validate issuer against the configured backend, not a fixed host

decode_agent_identity_jwt verifies signatures using JWKS fetched from the caller-provided chatgpt_base_url, but still hard-codes issuer to https://chatgpt.com/.... In non-prod/custom deployments (e.g. localhost or staging), correctly signed tokens from that backend can be rejected solely due to issuer mismatch, breaking AgentIdentity auth load/login flows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex-backend currently always sets issuer to chatgpt.com regardless of the deployment, this should be fine.

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct AgentIdentityJwtClaims {
pub iss: String,
pub aud: String,
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 Support JWT aud array form when decoding claims

AgentIdentityJwtClaims models aud as String, but JWT aud is valid as either a string or an array. If backend tokens emit aud as an array, verification may pass audience checks but deserialization still fails, causing valid AgentIdentity tokens to be rejected at auth load.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i believe this isnt a concern given you resolved twice, but can you briefly note why? on first pass it seems like a legit finding

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes! Since we issue the JWT claims on codex-backend, we guarantee that aud will just be a string.

efrazer-oai added a commit that referenced this pull request Apr 27, 2026
## Summary

Auth loading used to expose synchronous construction helpers in several
places even though some auth sources now need async work. This PR makes
the auth-loading surface async and updates the callers to await it.

This is intentionally only plumbing. It does not change how
AgentIdentity tokens are decoded, how task runtime ids are allocated, or
how JWT signatures are verified.

## Stack

1. **This PR:** [refactor: make auth loading
async](#19762)
2. [refactor: load AgentIdentity runtime
eagerly](#19763)
3. [feat: verify AgentIdentity JWTs with
JWKS](#19764)

## Important call sites

| Area | Change |
| --- | --- |
| `codex-login` auth loading | `CodexAuth` and `AuthManager`
construction paths now await auth loading. |
| app-server startup | Auth manager construction is awaited during
initialization. |
| CLI/TUI/exec/MCP/chatgpt callers | Existing auth-loading calls now
await the same behavior. |
| cloud requirements storage loader | The loader becomes async so it can
share the same auth construction path. |
| auth tests | Tests that load auth now run in async contexts. |

## Testing

Tests: targeted Rust auth test compilation, formatter, scoped Clippy
fix, and Bazel lock check.
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-eager-runtime branch 3 times, most recently from 381d0dc to 248b00b Compare April 27, 2026 20:01
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch 2 times, most recently from 3d6fc02 to da21113 Compare April 27, 2026 21:59
Comment thread codex-rs/agent-identity/src/lib.rs
.find(&kid)
.with_context(|| format!("agent identity JWT kid {kid} is not trusted"))?;
let decoding_key = DecodingKey::from_jwk(jwk).context("failed to build JWT decoding key")?;
let mut validation = Validation::new(Algorithm::RS256);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

@efrazer-oai efrazer-oai marked this pull request as ready for review April 27, 2026 22:54
@efrazer-oai efrazer-oai requested a review from a team as a code owner April 27, 2026 22:54
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: da211139fe

ℹ️ 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 thread codex-rs/agent-identity/src/lib.rs Outdated

pub fn agent_identity_jwks_url(chatgpt_base_url: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/wham/agent-identities/jwks")
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 Build JWKS URL for /api/codex bases correctly

agent_identity_jwks_url always appends /wham/agent-identities/jwks to the configured base. With a Codex-API base like https://.../api/codex, this produces .../api/codex/wham/agent-identities/jwks, but JWKS is served at .../api/codex/agent-identities/jwks. AgentIdentity JWT verification will fail in Codex-API style deployments, breaking auth load/login.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

legit? not legit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

legit! thought i'd fixed that locally, changed it to fork similar to how the MCP app-server calls do it.

@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from da21113 to 849b6c8 Compare April 28, 2026 01:51
@efrazer-oai efrazer-oai changed the base branch from dev/efrazer/agent-identity-eager-runtime to dev/efrazer/agent-identity-authapi-base-url April 28, 2026 01:51
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-authapi-base-url branch from 8e394fa to 6a66027 Compare April 28, 2026 03:13
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from 849b6c8 to 1e74331 Compare April 28, 2026 03:15
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-authapi-base-url branch from 6a66027 to 1740e94 Compare April 28, 2026 03:18
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from 1e74331 to 2acec7d Compare April 28, 2026 03:19
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-authapi-base-url branch from 1740e94 to e733e1e Compare April 28, 2026 03:23
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch 2 times, most recently from 1dabc3a to b1c8070 Compare April 28, 2026 03:24
@efrazer-oai efrazer-oai requested a review from sayan-oai April 28, 2026 03:33
efrazer-oai added a commit that referenced this pull request Apr 28, 2026
## Summary

AgentIdentity auth previously registered the process task lazily behind
a `OnceCell`. That meant the auth object could be constructed before its
runtime task binding was known.

This PR makes AgentIdentity auth load the runtime task at auth load time
and stores the resulting process task id directly on the auth object.
The model-provider call path can then read a concrete task id instead of
handling a missing lazy value.

## Stack

1. [refactor: make auth loading
async](#19762) (merged)
2. **This PR:** [refactor: load AgentIdentity runtime
eagerly](#19763)
3. [fix: configure AgentIdentity AuthAPI base
URL](#19904)
4. [feat: verify AgentIdentity JWTs with
JWKS](#19764)

## Important call sites

| Area | Change |
| --- | --- |
| `AgentIdentityAuth::load` | Registers the process task during auth
loading and stores `process_task_id`. |
| `CodexAuth::from_agent_identity_jwt` | Awaits AgentIdentity auth
loading. |
| model-provider auth | Reads a concrete `process_task_id` instead of an
optional lazy value. |
| AgentIdentity auth tests | Mock task registration now covers eager
runtime allocation. |

## Design decisions

AgentIdentity auth now treats task registration as part of constructing
a usable auth object. That matches how callers use the value: once auth
is present, the model-provider path expects the task-scoped assertion
data to be ready.

## Testing

Tests: targeted Rust auth test compilation, formatter, scoped Clippy
fix, and Bazel lock check.
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from b1c8070 to 674e153 Compare April 28, 2026 05:04
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-authapi-base-url branch from dffde4b to 326798a Compare April 28, 2026 05:34
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from 5644ec2 to 0758599 Compare April 28, 2026 05:35
efrazer-oai added a commit that referenced this pull request Apr 28, 2026
## Summary

AgentIdentity runtime loading currently registers tasks against a single
hardcoded AuthAPI base URL. That works for production, but local and
staging validation may need registration to target a different
authapi-login-provider without baking internal staging service URLs into
the OSS binary.

This PR adds a small config surface for
`agent_identity_authapi_base_url` and threads it through the existing
auth-loading path as a direct argument. Explicit config wins. Without
config, task registration keeps using the production AuthAPI URL,
matching the current default behavior.

## Stack

1. #19762 - `refactor: make auth loading async` (merged)
2. #19763 - `refactor: load agent identity runtime eagerly`
3. This PR - `fix: configure AgentIdentity AuthAPI base URL`
4. #19764 - `feat: verify agent identity JWTs with JWKS`

## Design decisions

- Keep the existing auth-loading shape and pass the new value as an
argument. This avoids another wrapper loader and keeps the call path
readable.
- Add config instead of embedding internal staging URLs. Environments
that need a non-production AuthAPI can configure it explicitly.
- Keep the default AuthAPI registration URL as production.
`chatgpt_base_url` remains separate and is used by the follow-up JWKS
verification PR for fetching public keys from the ChatGPT backend route.
- Resolve the AuthAPI base URL inside AgentIdentity loading, because
task registration is the only consumer of this value.

## Testing

Tests: targeted Rust checks, AgentIdentity auth tests, config schema
regeneration, formatter/fix pass, and whitespace diff check.
Base automatically changed from dev/efrazer/agent-identity-authapi-base-url to main April 28, 2026 15:06
@efrazer-oai efrazer-oai force-pushed the dev/efrazer/agent-identity-jwt-verify branch from 0758599 to 09a65cb Compare April 28, 2026 15:19
@efrazer-oai efrazer-oai merged commit f6797c3 into main Apr 28, 2026
35 of 36 checks passed
@efrazer-oai efrazer-oai deleted the dev/efrazer/agent-identity-jwt-verify branch April 28, 2026 16:56
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 28, 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.

3 participants