[TT-17174] OAuth2 scope check (per-operation / per-MCP-primitive)#8203
Conversation
|
This pull request introduces a significant enhancement to the OAuth2 security scheme, enabling per-operation and per-MCP-primitive scope checking. This allows API definitions to specify granular authorization requirements for each endpoint directly within the OpenAPI (OAS) document, moving away from a single, API-wide scope policy. The core of the change is the ability to read and enforce scopes from the standard OAS To support this, the API definition has been extended, and the gateway's OAuth2 middleware has been substantially updated to dynamically resolve and enforce these fine-grained security requirements on a per-request basis. The implementation also correctly handles coexistence with other security schemes; an operation secured by a non-OAuth2 scheme (like JWT) will not be blocked by this new middleware. Files Changed AnalysisThe changes are concentrated in the
Architecture & Impact Assessment
Authorization Flow Diagramgraph TD
A[Request Arrives] --> B{OAuth2 Scope Check Enabled?};
B -- No --> C[Skip Scope Check];
B -- Yes --> D[Find OAS Operation/Primitive via Router];
D -- Not Found --> C;
D -- Found --> E[Resolve Required Scopes based on scopeSource];
E --> F{Are scopes required?};
F --|"No (e.g., other auth scheme)"|--> C;
F -- Yes --> G[Extract Scopes from Token];
G --> H{Token satisfies any scope alternative?};
H -- Yes --> I[Allow Request];
H -- No --> J["Deny Request (403 Insufficient Scope)"];
Scope Discovery & Context Expansion
Metadata
Powered by Visor from Probelabs Last updated: 2026-06-03T11:32:22.927Z | Triggered by: pr_updated | Commit: 5546404 💡 TIP: You can chat with Visor using |
Security Issues (1)
✅ Architecture Check PassedNo architecture issues found – changes LGTM. Performance Issues (1)
Quality Issues (3)
Powered by Visor from Probelabs Last updated: 2026-06-03T11:31:57.802Z | Triggered by: pr_updated | Commit: 5546404 💡 TIP: You can chat with Visor using |
59534f8 to
4bccd2b
Compare
897b39b to
39e200e
Compare
4bccd2b to
78cca80
Compare
39e200e to
840f356
Compare
78cca80 to
f3ba544
Compare
840f356 to
f79b58e
Compare
f3ba544 to
f43707f
Compare
f79b58e to
f3bdd76
Compare
f43707f to
da17d5a
Compare
f3bdd76 to
3453698
Compare
4961392 to
f0154dd
Compare
3453698 to
09a4f6b
Compare
f0154dd to
f624baf
Compare
09a4f6b to
6a20e30
Compare
f624baf to
0a3f94b
Compare
dbce1e2 to
708c30d
Compare
0a3f94b to
5042b9a
Compare
708c30d to
f82313a
Compare
f82313a to
b60992c
Compare
|
/review this comment |
|
/visor ask this comment The |
… 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
| case len(global) == 0: | ||
| return perOp | ||
| } | ||
| out := make([][]string, 0, len(perOp)*len(global)) |
| out := make([][]string, 0, len(perOp)*len(global)) | ||
| for _, p := range perOp { | ||
| for _, g := range global { | ||
| merged := make([]string, 0, len(p)+len(g)) |
🎯 Recommended Merge TargetsBased on JIRA ticket TT-17174: OAuth2 scope check — per-operation and per-MCP-primitive Fix Version: Tyk 5.14.0
Required:
📋 Workflow
|
🚨 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. |
|
…oauth2 (gateway) (#8209) Story 04 of the `oauth2` security-scheme series (stacked on #8203 / TT-17174). Spec: `token-exchange/JIRA_ISSUES/04-prm-new-structure/`. Moves RFC 9728 Protected Resource Metadata into the `oauth2` 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`, `scopesSupported`, `autoDeriveScopes`) + `GetWellKnownPath` / `IsAutoDeriveScopes`; `OAuth2.HasContent` and the oauth2 sub-block map-probe recognise the `protectedResourceMetadata` key. - `OAS.OAuth2PRMScopesSupported`: the hybrid `scopes_supported` — union of operator-supplied `scopesSupported`, the scheme's `scopeCheck.scopes` baseline (flattened across alternatives), and (by default) the auto-derived per-operation / per-MCP-primitive `security:` scopes. - `x-tyk-api-gateway(.strict).json`: `X-Tyk-OAuth2-PRM` sub-schema. **gateway** - `APISpec.GetOAuth2PRMConfig` accessor. - `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**: `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_metadata` on 403/401, PRM-disabled omits it). 🤖 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-17175" title="TT-17175" target="_blank">TT-17175</a> </summary> | | | |---------|----| | Status | Ready for Testing | | Summary | Protected Resource Metadata — new structure inside `oauth2` | Generated at: 2026-05-27 18:07:40 </details> <!---TykTechnologies/jira-linter ends here--> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>



Summary
Extends the new
oauth2security scheme's scope check (TT-17173 added the API-level baseline) to read per-operation OASsecurity:declarations and per-MCP-primitivesecurityarrays. Operators declare required scopes once in the OAS document; the gateway enforces them per request. Grammar is OR-of-AND (security:is OR across alternatives, AND across the scopes within an alternative).scopeSourcecomposes the two paths:union(default) requires both,operationignores the global baseline,globalignores per-op. On a missing scope the 403 cites the first declared alternative in the RFC 6750WWW-Authenticate: Bearer error="insufficient_scope" scope="…"challenge; MCP/JSON-RPC routes get the failure wrapped in a JSON-RPC 2.0 error envelope. An operation guarded by another scheme (e.g. JWT) contributes an empty AND-group, so the oauth2 middleware stands down for that route — preserving coexistence. The whole check is gated onoauth2.scopeCheck.enabled(default off), so existing APIs are unaffected.apidef:
MCPPrimitive.Security(openapi3.SecurityRequirements),DeriveOAuth2Scopes/SortedOAuth2Scopes(walk root + per-op + per-MCP-primitivesecurity:),mcpTools[].securityschema delta. gateway:requiredScopeAlternativesForRequest(per-op + global composition perscopeSource, union as cross-product viaunionScopeAlternatives),perOperationScopeAlternatives(MCP primitive via JSONRPC routing state + REST op via OAS path lookup),appendOAuth2Alternatives,oauth2ScopeSource,dedupPreserveOrder. Unit +StartTestend-to-end tests.Story
Companion PRs
This story spans three repos and ships together:
Reviewers: please block until all three are green.
Stacked on
Base is
TT-17173-scope-check-api-level(Story 02 / TT-17173). Review/merge TT-17173 first, then this can be retargeted tomaster.Test plan
go test ./apidef/oas/ -run 'OAuth2|MCPPrimitive'andgo test ./gateway/ -run OAuth2— green locallypytest tests/api/tests/mcp/mcp_oauth2_per_op_scope_check_test.py— 15/15 against a live dashboard + gateway + Keycloaknewman runon the Story-03 Postman collection — 17/17 against the live stack--repeat-each=5 --workers=1— 5/5 chromium + 5/5 firefoxNote: pre-existing unrelated failure
apidef/oas TestAPIContext_getValidationOptionsFromConfigpanics on the parent branch too.Risk
Schema delta is confined to
apidef/mcp/schema/x-tyk-api-gateway.json(mcpTools[].security) — per-op RESTsecurity:is native OpenAPI, read at runtime with no x-tyk-extension delta. The map-probe disambiguator and the new helpers extend Story 02's structures without changing existing paths. Coexistence is covered by tests (an op secured by another scheme is not blocked by oauth2). Default-off gating means a no-op for any API that doesn't opt intoscopeCheck.🤖 Generated with Claude Code
Ticket Details
TT-17174
Generated at: 2026-06-03 11:30:32