Repository navigation
feat: add the id-jag assertion validation chain - #134
Conversation
Claude.ai only reaches this server through DCR plus authorization_code, so every user sits through a consent screen. Enterprise-managed auth replaces that with a signed identity assertion that the organization's issuer exchanges at /token, and none of the machinery to validate one existed. Validation comes first: the nine ordered checks the EMA extension defines, a per-issuer JWKS cache with its own timeout budget, replay protection keyed on (issuer, jti), trustedIssuers and staticClients in config, and the WWW-Authenticate challenge on 401. Roles come from config, never from the assertion. The jwt-bearer arm of /token is not here yet, so no production path changes. Workflows also get concurrency: a new push cancels the branch's older builds, while publish and deploy serialize instead of cancelling. Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical issues can cancel sibling release builds, fail CI on unused code, grant tenant roles without a tenant claim, and allow signing-key substitution through non-HTTPS JWKS retrieval.
6 open issues
Scope matrix cancellation by target · New Require tenant claim when configured tenant is specific · New Enforce HTTPS for discovery and JWKS redirects · New Uncalled validator modules fail Clippy with warnings denied · New Mark managed authorization guidance as planned · New Document replay risk across pod restarts · New
What changed in this PR
Adds the ID-JAG validation foundation for enterprise-managed authorization in the Rust MCP server. The jwt-bearer token-endpoint integration remains deferred.
Changes:
- Adds assertion validation, bounded JWKS caching, replay protection, and trusted-issuer/static-client configuration.
- Adds authentication challenges, reload handling, tests, and operator documentation.
- Adds concurrency controls to release and publishing workflows.
| File | Description |
|---|---|
src/server_auth/oauth_as/token.rs |
Updates test configuration fields. |
src/server_auth/oauth_as/test_helpers.rs |
Adds signing fixtures and fake issuer servers. |
src/server_auth/oauth_as/state.rs |
Adds static clients, replay tracking, and detailed cleanup reports. |
src/server_auth/oauth_as/register.rs |
Updates registration test configuration. |
src/server_auth/oauth_as/provider.rs |
Updates provider test configuration. |
src/server_auth/oauth_as/mod.rs |
Declares validation modules and exports shared types. |
src/server_auth/oauth_as/metadata.rs |
Updates metadata test configuration. |
src/server_auth/oauth_as/jwks.rs |
Adds bounded signing-key retrieval and caching. |
src/server_auth/oauth_as/e2e_tests.rs |
Updates end-to-end test configuration. |
src/server_auth/oauth_as/config.rs |
Adds trusted issuers, static clients, and validation rules. |
src/server_auth/oauth_as/authorize.rs |
Updates authorization test configuration. |
src/server_auth/oauth_as/assertion.rs |
Implements assertion checks and security tests. |
src/server_auth/mod.rs |
Converts configured static clients without mutating state. |
src/serve/stdio.rs |
Updates boot invariants for stdio. |
src/serve/reload.rs |
Applies static clients and challenges; reports issuer restart requirements. |
src/serve/reload_tests.rs |
Tests client preservation, restart reporting, and challenges. |
src/serve/http.rs |
Adds authentication challenges and state lifecycle handling. |
docs/reference/config-file.md |
Documents new authorization configuration fields. |
docs/howto/oauth-as.md |
Adds managed-authorization and deployment guidance. |
.github/workflows/release.yml |
Adds per-tag release concurrency. |
.github/workflows/release-beta.yml |
Adds build cancellation and serialized publishing. |
.github/workflows/publish-homebrew.yml |
Serializes Homebrew publishing. |
.github/workflows/publish-crate.yml |
Adds per-ref crate publishing concurrency. |
Review effort: Balanced
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved security flaws allow assertions scoped to other resources to be accepted and keys explicitly restricted from signature verification to verify identity assertions.
7 open issues
Reject JWKS keys marked incompatible with signature verification · New Enforce signed resource restrictions before accepting assertions · New Uncalled validator modules fail Clippy with warnings denied Coalesce issuer refreshes and back off after failures · New Advertise the JWT-bearer grant in authorization-server metadata · New Document replay risk across pod restarts Mark managed authorization guidance as planned
3 resolved since last review
Review effort: Balanced
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Signed scope restrictions are discarded during token issuance, allowing read-only assertions to produce write-capable sessions when the issuer’s configured roles permit writes.
3 open issues
7 resolved since last review
Enforce signed resource restrictions before accepting assertions Reject JWKS keys marked incompatible with signature verification Uncalled validator modules fail Clippy with warnings denied Advertise the JWT-bearer grant in authorization-server metadata Coalesce issuer refreshes and back off after failures Document replay risk across pod restarts Mark managed authorization guidance as planned
Review effort: Balanced
| .await | ||
| .map_err(|why| err(StatusCode::BAD_REQUEST, "invalid_grant", why))?; | ||
|
|
||
| 8 let a 10BC8 ccess_token = mint_access_token(ctx, &client_id, &validated.subject, &validated.roles)?; |
| if claims | ||
| .nbf | ||
| .is_some_and(|nbf| nbf > now.saturating_add(MAX_CLOCK_SKEW_SECONDS)) | ||
| { | ||
| return Err("assertion is not valid yet".to_string()); |
| // Taken after the hit check, not before, so a cached key is never | ||
| // made to queue behind someone else's refresh. | ||
| let gate = self.gate(issuer); | ||
| let mut last_failure = gate.lock().await; |



Claude.ai only reaches this server through DCR plus authorization_code, so every user sits through a consent screen. Enterprise-managed auth replaces that with a signed identity assertion that the organization's issuer exchanges at /token, and none of the machinery to validate one existed.
Validation comes first: the nine ordered checks the EMA extension defines, a per-issuer JWKS cache with its own timeout budget, replay protection keyed on (issuer, jti), trustedIssuers and staticClients in config, and the WWW-Authenticate challenge on 401. Roles come from config, never from the assertion. The jwt-bearer arm of /token is not here yet, so no production path changes.
Workflows also get concurrency: a new push cancels the branch's older builds, while publish and deploy serialize instead of cancelling.