agentic-community / agentic-community/mcp-gateway-registry

[Phase 3] RFC 8707 resource parameter enforcement + token proxy (closes #889)

Aperta
#991 1 commento 0 reazioni 1 assegnatario Rivendicata da @omrishiv Vedi su GitHub
authentication enhancement mcp-spec oauth security subtask
Lingua principale
Python
Stelle
911
Fork
234
Merge medio
1g 11h
PR unite (30g)
62

Descrizione

# Sub-issue E: RFC 8707 `resource` parameter enforcement + token proxy (IdP-signed tokens)

**Parent:** #988
**Labels:** `enhancement`, `oauth`, `mcp-spec`, `authentication`, `credentials-management`, `subtask`
**Phase:** 3
**Depends on:** A
**Blocks:** B, C, D, H, closes #889 as part of acceptance
**Absorbs:** #614 Phase 2-3

---

## Summary

Stand up the `/oauth/token` proxy in the Registry Gateway, enforce [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) `resource` parameter semantics end-to-end, and switch MCP-data-plane token validation from the current self-signed 8h JWT to IdP-signed tokens. This is the phase that makes refresh and revocation actually work, and it closes #889 as a side-effect.

## Background

Today's auth flow:

- Operator clicks Connect in the UI → auth server mints a self-signed JWT with a hardcoded 8h TTL, no refresh token.
- Operator pastes it into `.mcp.json` / `mcp.json`.
- Assistant uses the Bearer token until expiry; there is no refresh and no revocation propagation from the upstream IdP.
- `MAX_TOKEN_LIFETIME_HOURS=24` is defined but never consulted (#889).
- `expires_in_hours` on the registration request is ignored.

Problems:

1. Revocation in the upstream IdP has no effect on the Registry until the 8h timer elapses.
2. Refresh is manual (re-paste).
3. Tokens are not scoped to a specific MCP server; any token hitting the gateway can reach any path the user is entitled to, with no resource-parameter check.
4. `#889` TTL enforcement can't be cleanly implemented on top of the self-signed path without also fixing the proxy.

## Scope

### In scope

- **`POST /oauth/token`** as a proxy for `authorization_code` and `refresh_token` grants to the configured IdP:
- Preserves PKCE `code_verifier` on the way through.
- Preserves `resource` parameter on the way through (RFC 8707).
- Enforces `resource` matches a Registry-registered MCP server canonical URI or a Registry-own resource.
- On success, returns the IdP-signed `access_token` and `refresh_token` to the assistant; the gateway does not re-mint.
- **Inbound token validation** on MCP-facing paths:
- Validate signature via the configured IdP's JWKS (cached with key rotation support).
- Validate `aud` claim: MUST equal the canonical URI of the MCP server being called (i.e., the path the request is going to).
- Validate `iss` claim against provider config.
- Validate `exp` / `nbf`.
- On any failure, 401 with `WWW-Authenticate` (sub-issue A's middleware takes care of the header).
- **Retire self-signed 8h tokens** on the MCP data plane. The web-UI login flow can keep its own session cookies; MCP data plane moves to IdP-signed.
- **Per-server `expires_in_hours` at registration**: add a field on the `register` model, enforce `MAX_TOKEN_LIFETIME_HOURS` as the ceiling, honor lower values per server. Closes #889.
- **Revocation observability**: add a metric for `aud` mismatches and signature failures so operators can see revocation events working in real time.
- **Tests**:
- Unit: `aud` mismatch, `iss` mismatch, expired, wrong signature, missing `resource` on `/authorize`, `resource` not in registered set, TTL clamp against `MAX_TOKEN_LIFETIME_HOURS`.
- Integration: end-to-end PKCE against a local IdP (Keycloak in docker-compose), revocation in IdP → next request 401 within JWKS cache refresh interval.

### Out of scope

- CIMD consumer behavior on `/authorize` → sub-issue C.
- Token exchange (RFC 8693) grant type → sub-issue D.
- Entra v1 scope format nuances → sub-issue F.
- Domain B backend token handling → declined, see #988 §"What we considered and declined".

## Design notes

- Keep the proxy thin. It forwards to the configured IdP's `/token` endpoint and returns the IdP's response verbatim, except for:
- Rejecting `resource` values that don't map to a Registry-registered server.
- Clamping `expires_in` down to the per-server / registry max.
- `aud` enforcement is the single most important hardening gain here. Today the gateway accepts any signed JWT regardless of audience; after this lands, a token for `/server-a/mcp` fails against `/server-b/mcp`.
- JWKS cache TTL balances revocation latency vs. IdP load. Default to 5 minutes; operator-configurable.
- The switch off the self-signed path is a breaking change for any existing deployment that is still relying on the UI-pasted JWT. Ship behind a feature flag for one release; flip default in the next.

## Acceptance criteria

- [ ] `POST /oauth/token` proxies `authorization_code` and `refresh_token` grants to the configured IdP, preserving PKCE and `resource`.
- [ ] `resource` parameter is required on `/authorize` and `/token` for MCP-facing flows; rejected if not present or not in registered set.
- [ ] MCP-facing paths validate `aud` matches the path; mismatch returns 401 + `WWW-Authenticate`.
- [ ] JWKS cache honors key rotation (kid match + re-fetch on unknown kid).
- [ ] `expires_in_hours` in registration honored; `MAX_TOKEN_LIFETIME_HOURS` enforced as ceiling (closes #889).
- [ ] Metrics for: successful exchanges, `aud` mismatch count, signature failure count, TTL clamp count.
- [ ] Self-signed 8h token path retired on the MCP data plane behind a feature flag; default flipped in the release after this one.
- [ ] Integration test using Keycloak: authorization code + refresh + revocation all behave correctly.
- [ ] Docs page in [docs/](docs/) describing the new token model and the migration from self-signed tokens.

## Risks and open questions

- **Breaking change for operators.** The UI-paste workflow is going away for MCP paths. Communicate via release notes, one full release behind a flag before flipping default.
- **IdP quirks.** Entra v1 does not always include all the fields we want in JWTs. Sub-issue F explicitly handles the scope format; coordinate so scope mapping for Entra is unified.
- **JWKS downtime.** If the IdP's JWKS endpoint is down, valid tokens start failing. Mitigation: cache persists across JWKS fetch failures with a bounded staleness window; alert on sustained JWKS fetch failures.
- **`aud` format inconsistencies across IdPs.** Some IdPs use the resource URI, others use a client-id-like opaque string. Normalize in provider-specific adapters, not in the core validation.

## References

- [RFC 8707 Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707)
- [MCP 2025-06-18 authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- Issue #889 (TTL enforcement)
- Issue #614 (umbrella tactical)
- [auth_server/providers/entra.py](auth_server/providers/entra.py) — dual-audience pattern already present
- Recommendation doc §6 Phase 3: [.scratchpad/coding-assistant-oauth/recommendation-2026-05-04.md](.scratchpad/coding-assistant-oauth/recommendation-2026-05-04.md)

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.