[TT-17175] OAuth2 Protected Resource Metadata — new structure inside oauth2 (gateway)#8209
Conversation
|
This PR introduces two major enhancements to the OAuth2 security scheme. The primary change is the relocation of the Protected Resource Metadata (PRM) configuration to be scheme-specific. The second, more substantial change is the introduction of a new, Enterprise-only RFC 8693 Token Exchange feature. First, the PR moves the RFC 9728 Protected Resource Metadata (PRM) configuration from a top-level block ( Second, this PR introduces RFC 8693 Token Exchange as a new feature. A Files Changed AnalysisThis is a large pull request with over 4,000 additions across 30 files. The changes are concentrated in API definition structures and gateway middleware logic.
Architecture & Impact Assessment
Token Exchange Middleware FlowsequenceDiagram
participant Client
participant Gateway
participant IdP
participant Upstream
Client->>+Gateway: Request with Inbound Token
Gateway->>Gateway: OAuth2Middleware: Parses token, extracts claims
Note over Gateway: Prepares oauth2common.State in request context
Gateway->>Gateway: OAuth2TokenExchangeMiddleware (EE-only): Reads state
Gateway->>+IdP: POST /token (RFC 8693 Exchange Request)
IdP-->>-Gateway: Responds with Exchanged Token
Note over Gateway: Replaces Authorization header with Exchanged Token
Gateway->>+Upstream: Proxies request with new token
Upstream-->>-Gateway: Response
Gateway-->>-Client: Response
PRM Precedence Logicgraph TD
A[Request to PRM .well-known path] --> B[PRMMiddleware];
B --> C{Is new `oauth2.protectedResourceMetadata` configured?};
C -- Yes --> D[Serve PRM from new config];
C -- No --> E{Is legacy `authentication.protectedResourceMetadata` configured?};
E -- Yes --> F[Serve PRM from legacy config];
E -- No --> G[Pass request to next middleware];
D --> H[End Request];
F --> H;
Scope Discovery & Context Expansion
Metadata
Powered by Visor from Probelabs Last updated: 2026-06-08T22:07:31.124Z | Triggered by: pr_updated | Commit: c8c412f 💡 TIP: You can chat with Visor using |
Security Issues (2)
Security Issues (2)
Performance Issues (2)
Quality Issues (1)
Powered by Visor from Probelabs Last updated: 2026-06-08T22:06:51.235Z | Triggered by: pr_updated | Commit: c8c412f 💡 TIP: You can chat with Visor using |
361a9a3 to
e4ee669
Compare
897b39b to
39e200e
Compare
e4ee669 to
dda4236
Compare
39e200e to
840f356
Compare
dda4236 to
a6ed678
Compare
840f356 to
f79b58e
Compare
a6ed678 to
13a959e
Compare
f79b58e to
f3bdd76
Compare
13a959e to
c26dc11
Compare
f3bdd76 to
3453698
Compare
c26dc11 to
5e8d053
Compare
3453698 to
09a4f6b
Compare
5e8d053 to
134f267
Compare
f82313a to
b60992c
Compare
0ff24c9 to
11b3da6
Compare
11b3da6 to
e2c1512
Compare
… MCP primitive Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
MCP tool, resource and prompt names share no namespace. Looking the primitive up by name across all three maps let a same-named primitive of another type fold its `security:` in as an extra OR-alternative — a resource-only token could then authorize a tools/call. Select the primitive map by the routing-state PrimitiveType. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
7b49eee to
1d0d8ad
Compare
…oauth2 Move RFC 9728 Protected Resource Metadata into the oauth2 security scheme: authentication.securitySchemes[<name>].oauth2.protectedResourceMetadata. The deprecated top-level authentication.protectedResourceMetadata block keeps working unchanged; when both are configured the new one is the sole authority (Story 05 handles migration). apidef/oas: - OAuth2PRM struct (enabled, wellKnownPath, resource, authorizationServers, autoDeriveScopes) + GetWellKnownPath / IsAutoDeriveScopes. - OAuth2.HasContent and the oauth2 sub-block map-probe recognise the protectedResourceMetadata key. - OAS.OAuth2PRMScopesSupported: scopes_supported is the operator-authored catalog — the OAS security scheme's flows.<flow>.scopes keys — unioned (when autoDeriveScopes is nil/true, the default) at serve time with every scope referenced by an oauth2 scheme in any security: array (root, per-operation, per-MCP-primitive). The union is read-only; the OAS document is never mutated. - fillOAuth2OASScheme no longer derives flows.scopes from security: — an operator-authored security-scheme component is preserved verbatim, and only a minimal empty-catalog skeleton is synthesised when none exists. - x-tyk-api-gateway(.strict).json: X-Tyk-OAuth2-PRM sub-schema. gateway: - APISpec.GetOAuth2PRMConfig accessor for the new location. - PRMMiddleware serves the new block (static assembly) when configured, short-circuiting the old block; always advertises bearer_methods_supported: ["header"] (RFC 9728 §2). - prmMetadataURL resolves the canonical PRM URL (new wins over old); the scope-check middleware appends a resource_metadata= parameter (RFC 9728 §5.1) to its insufficient_scope / invalid_token Bearer challenges when the API publishes a PRM document. Tests: OAuth2PRM struct, the catalog-plus-derive scopes_supported matrix, the catalog read across flow types, and no write-back to flows.scopes (apidef); well-known serving, custom path, autoDeriveScopes:false advertising only the catalog, new-wins-over-old, resource_metadata on 403/401, PRM-disabled omits it (gateway). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
e2c1512 to
79c1105
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
🎯 Recommended Merge TargetsBased on JIRA ticket TT-17175: Protected Resource Metadata — new structure inside Fix Version: Tyk 5.14.0
Required:
📋 Workflow
|
…guarantees (#8247) ## Summary Companion gateway PR for the Story 05 PRM auto-migration in tyk-analytics. The dashboard migration ([TT-17176 PR #5657](TykTechnologies/tyk-analytics#5657)) is non-destructive — it leaves the deprecated top-level `authentication.protectedResourceMetadata` in place and adds the new `securitySchemes[<name>].oauth2.protectedResourceMetadata` alongside, so the gateway must continue to honour both shapes during the transition. This PR: - **Marks the legacy struct deprecated** at both Go and JSON-Schema level so IDEs, `staticcheck SA1019`, OpenAPI Generator, Stoplight, Redocly, and the dashboard form generator all surface the deprecation. `Authentication.ProtectedResourceMetadata` and the `ProtectedResourceMetadata` type each carry `// Deprecated:` doc comments; all four `x-tyk-api-gateway` schema variants (`apidef/oas/schema/x-tyk-api-gateway.json`, `…strict.json`, `apidef/mcp/schema/x-tyk-api-gateway.json`, `apidef/streams/schema/x-tyk-api-gateway.json`) carry `"deprecated": true` on the legacy type definition. - **Pins runtime guarantees** the migration depends on: - PRM serves when the OAuth2 master is disabled — what lets the dashboard produce `oauth2.enabled: false` + `oauth2.protectedResourceMetadata.enabled: true` without silently turning on authentication that wasn't there before. - Legacy block fallback when the new per-scheme PRM sub-block is absent — what makes downgrade safe (operator removes the new block → gateway serves from the legacy top-level block). No behaviour change. Pure backwards-compatible documentation + test surface. ## Story - JIRA: [TT-17176](https://tyktech.atlassian.net/browse/TT-17176) ## Companion PRs - `tyk-analytics` — [#5657](TykTechnologies/tyk-analytics#5657) — dashboard startup migration + Python e2e. - `tyk-analytics-ui` (webclient) — no PR for this story; the optional UI banner / read-only legacy form is deferred. ## Test plan - [x] `go test ./apidef/oas/... -run TestProtectedResourceMetadata_SchemaMarkedDeprecated -count=1` - [x] `go test ./apidef/oas/... -run TestOAS_PRMBothBlocksRoundTrip -count=1` — both blocks survive marshal/unmarshal with the OAuth2 master staying `false`. - [x] `go test ./gateway/... -run TestOAuth2PRM_ServesWhenOAuth2MasterDisabled -count=1` - [x] `go test ./gateway/... -run TestOAuth2PRM_FallsBackToLegacyWhenNewBlockAbsent -count=1` ## Risk - **Behaviour-neutral.** No runtime code path changes — the new tests pin existing behaviour the migration relies on. Schema-level `"deprecated": true` is advisory metadata; clients that don't read it see no change. - The earlier [#8212](#8212) on this branch carried an `Authentication.PRMMigratedToOAuth2` marker field that has since been removed in favour of presence-based idempotency on the dashboard side — that PR was closed; this PR replaces it with a strictly narrower scope (deprecation + runtime-guarantee tests, no marker field). 🤖 Generated with [Claude Code](https://claude.com/claude-code) [TT-17176]: https://tyktech.atlassian.net/browse/TT-17176?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!---TykTechnologies/jira-linter starts here--> ### Ticket Details <details> <summary> <a href="https://tyktech.atlassian.net/browse/TT-17176" title="TT-17176" target="_blank">TT-17176</a> </summary> | | | |---------|----| | Status | Ready for Testing | | Summary | Protected Resource Metadata — auto-migration of existing customers | Generated at: 2026-05-27 18:07:46 </details> <!---TykTechnologies/jira-linter ends here--> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…#8255) ## Summary - EE-only `TokenExchangeMiddleware` wraps every OAuth2-authenticated request: reads `oauth2common.State` from context (set by introspection+scope-check), posts RFC 8693 form to the matched provider's `tokenEndpoint`, replaces `Authorization: Bearer` with the exchanged token before forwarding upstream. - OSS noop stub logs a one-shot warning; `oauth2BuildIsEE` constant lets gating tests assert the correct boundary without live requests. - `ValidateOAuth2Schemes` enforces at API-load time: no reserved `customParams` keys, unique provider names per scheme, no issuer overlap across providers, non-empty `tokenEndpoint` + `clientAuth.clientId`. - Per-operation `OAuth2Exchange` override (`audience`, `scopes`) lets individual routes narrow the exchanged token scope beyond the provider default. - All four JSON schemas (`x-tyk-api-gateway`, strict, streams, mcp) updated with `tokenExchange` + `X-Tyk-OAuth2-Exchange` type definitions. ## Test plan - [ ] `go test ./internal/oauth2common/ ./apidef/oas/ -count=1` — unit + round-trip + validation tests pass - [ ] `go test -run "TestOAuth2|TestTokenExchange|TestFlatten|TestDedup|TestGating" ./gateway/ -count=1` — OSS/EE boundary + gating tests pass - [ ] `go test ./apidef/oas/ -run TestXTykGateway_Lint -count=1` — all four schemas lint clean - [ ] `go build ./... && go build -tags "ee dev" ./...` — both build flavors compile - [ ] Python e2e: `mcp_oauth2_token_exchange_core_test.py` against live stack (see companion tyk-analytics PR) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!---TykTechnologies/jira-linter starts here--> ### Ticket Details <details> <summary> <a href="https://tyktech.atlassian.net/browse/TT-17177" title="TT-17177" target="_blank">TT-17177</a> </summary> | | | |---------|----| | Status | In Code Review | | Summary | Token Exchange — core flow (RFC 8693) | Generated at: 2026-05-29 17:44:14 </details> <!---TykTechnologies/jira-linter ends here--> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
…ived/static TTL) (#8269) ## Summary - Redis-backed cache for exchanged tokens, keyed per token shape (`subject_id + api_id + audience + sorted scopes + provider_name`; `actor_id` reserved as a stable empty placeholder until Story 07 lands). - `internal/oauth2common/ttl.go`: derived-or-static TTL with safety margin. `derived` clamps to the exchanged token's `expires_in` minus margin; `static` clamps to `min(timeout, expires_in)` so upstream never sees an expired token. - `gateway/oauth2_cache.go`: Redis cache adapter — lookup, set-with-TTL, encrypted-at-rest serialise/deserialise. Per-API namespacing in the key isolates hot keys. - `ee/middleware/oauth2tokenexchange/cache.go`: single-flight wrapper — concurrent identical exchanges collapse to one IdP call; bounded waiter timeout so a failed leader doesn't deadlock the followers. - `exchange.go` integration: cache lookup before the IdP call, cache write after a successful exchange. Structured logs on miss (`info`, no subject) and hit (`debug`, with `ttl_remaining`). - `apidef/oas/oauth2.go`: `OAuth2ExchangeCache` struct + `derived`/`static` mode constants; `cache` sub-schema added to all four JSON schemas (`x-tyk-api-gateway`, strict, streams, mcp). Save-time validation rejects `mode` ∉ {`derived`, `static`}. ## Test plan - [ ] `go test ./internal/oauth2common/ -count=1` — cache-key "different inputs → different keys" matrix + TTL clamp formulas - [ ] `go test ./apidef/oas/ -count=1` — cache-block round-trip + `mode` validation - [ ] `go test -run "TestOAuth2Cache|TestSingleFlight|TestTokenExchange" ./gateway/ ./ee/middleware/oauth2tokenexchange/ -tags "ee dev" -count=1` — cache hit/miss, single-flight, encryption-at-rest - [ ] `go build ./... && go build -tags "ee dev" ./...` — both build flavors compile - [ ] Python e2e: `mcp_oauth2_token_exchange_cache_test.py` against live stack (see companion tyk-analytics PR #5762) Companion PRs: tyk-analytics #5762 · webclient #4395. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!---TykTechnologies/jira-linter starts here--> ### Ticket Details <details> <summary> <a href="https://tyktech.atlassian.net/browse/TT-17179" title="TT-17179" target="_blank">TT-17179</a> </summary> | | | |---------|----| | Status | In Code Review | | Summary | Token Exchange — caching | Generated at: 2026-06-03 17:34:48 </details> <!---TykTechnologies/jira-linter ends here--> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
🚨 Jira Linter FailedCommit: The Jira linter failed to validate your PR. Please check the error details below: 🔍 Click to view error detailsNext Steps
This comment will be automatically deleted once the linter passes. |
|



Story 04 of the
oauth2security-scheme series (stacked on #8203 / TT-17174). Spec:token-exchange/JIRA_ISSUES/04-prm-new-structure/.Moves RFC 9728 Protected Resource Metadata into the
oauth2scheme:authentication.securitySchemes[<name>].oauth2.protectedResourceMetadata. The deprecated top-levelauthentication.protectedResourceMetadatablock keeps working unchanged; when both are configured the new one is the sole authority (Story 05 handles migration).apidef/oas
OAuth2PRMstruct (enabled,wellKnownPath,resource,authorizationServers,scopesSupported,autoDeriveScopes) +GetWellKnownPath/IsAutoDeriveScopes;OAuth2.HasContentand the oauth2 sub-block map-probe recognise theprotectedResourceMetadatakey.OAS.OAuth2PRMScopesSupported: the hybridscopes_supported— union of operator-suppliedscopesSupported, the scheme'sscopeCheck.scopesbaseline (flattened across alternatives), and (by default) the auto-derived per-operation / per-MCP-primitivesecurity:scopes.x-tyk-api-gateway(.strict).json:X-Tyk-OAuth2-PRMsub-schema.gateway
APISpec.GetOAuth2PRMConfigaccessor.PRMMiddlewareserves the new block (static assembly) when configured, short-circuiting the old block; always advertisesbearer_methods_supported: ["header"](RFC 9728 §2).prmMetadataURLresolves the canonical PRM URL (new wins over old); the scope-check middleware appends aresource_metadata=parameter (RFC 9728 §5.1) to itsinsufficient_scope/invalid_tokenBearer challenges when the API publishes a PRM document.Tests:
apidef/oas/oauth2_prm_test.go(struct + scopes-union matrix);gateway/mw_protected_resource_oauth2_test.go(well-known serving, custom path,autoDeriveScopes:false, new-wins-over-old,resource_metadataon 403/401, PRM-disabled omits it).🤖 Generated with Claude Code
Ticket Details
TT-17175
oauth2Generated at: 2026-05-27 18:07:40