Skip to content

[TT-17175] OAuth2 Protected Resource Metadata — new structure inside oauth2 (gateway)#8209

Merged
lghiur merged 14 commits into
masterfrom
TT-17175-prm-new-structure
Jun 9, 2026
Merged

[TT-17175] OAuth2 Protected Resource Metadata — new structure inside oauth2 (gateway)#8209
lghiur merged 14 commits into
masterfrom
TT-17175-prm-new-structure

Conversation

@lghiur

@lghiur lghiur commented May 12, 2026

Copy link
Copy Markdown
Collaborator

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

Ticket Details

TT-17175
Status Ready for Testing
Summary Protected Resource Metadata — new structure inside oauth2

Generated at: 2026-05-27 18:07:40

@probelabs

probelabs Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

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 (authentication.protectedResourceMetadata) to a nested location within the oauth2 security scheme (authentication.securitySchemes[<name>].oauth2.protectedResourceMetadata). The legacy top-level block is preserved for backward compatibility, with the new scheme-specific configuration taking precedence. As part of this, the gateway now appends a resource_metadata parameter to WWW-Authenticate headers on 401/403 responses, pointing clients to the PRM document for discovery, as specified in RFC 9728 §5.1.

Second, this PR introduces RFC 8693 Token Exchange as a new feature. A tokenExchange block can now be configured within an oauth2 security scheme. This allows the gateway to exchange an inbound subject token with an external Identity Provider (IdP) for a new token scoped for the upstream service. This functionality is implemented in a new OAuth2TokenExchangeMiddleware which is conditionally compiled for Enterprise Edition (EE) builds.

Files Changed Analysis

This 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.

  • API Definition (apidef): Core data structures for both the new PRM location (OAuth2PRM) and the new Token Exchange feature (OAuth2TokenExchange, OAuth2TokenExchangeProvider, etc.) are defined in apidef/oas/oauth2.go. The corresponding JSON schemas in apidef/mcp/schema/, apidef/oas/schema/, and apidef/streams/schema/ are updated to reflect these new structures.
  • Gateway Middleware (gateway & ee/middleware): The logic for the new PRM location is handled by prioritizing the new configuration. gateway/mw_oauth2.go is refactored to parse the inbound token and pass its state via request context to the new Token Exchange middleware. The core logic for token exchange resides in the new ee/middleware/oauth2tokenexchange/ directory, which is conditionally compiled.
  • Build System & Gating: New files gateway/mw_oauth2_exchange.go (OSS shim) and gateway/mw_oauth2_exchange_ee.go (EE implementation) use build tags to conditionally include the Token Exchange feature.
  • Testing: The PR adds extensive tests for both features, including apidef/oas/oauth2_prm_test.go for PRM, and apidef/oas/oauth2_token_exchange_test.go, gateway/mw_oauth2_gating_test.go for Token Exchange and its EE/OSS gating.

Architecture & Impact Assessment

  • What this PR accomplishes:

    1. Refactors the Protected Resource Metadata configuration to be a more intuitive, scheme-specific property.
    2. Enhances RFC 9728 compliance by adding the resource_metadata discovery parameter to Bearer challenges.
    3. Introduces a major new feature: RFC 8693 Token Exchange, allowing the gateway to act as an OAuth2 client to an IdP on behalf of the original caller.
  • Key technical changes introduced:

    • A new OAuth2PRM struct is added to the OAS definition, and the PRM middleware is updated to prioritize it.
    • A new OAuth2TokenExchange configuration structure is introduced, along with a new EE-only middleware that performs outbound HTTP requests to an IdP.
    • The OAuth2Middleware now acts as a pre-processor, parsing the inbound token and preparing a context State object for the downstream Token Exchange middleware.
    • Build tags are used to create different gateway behaviors for OSS vs. EE builds regarding this feature.
  • Affected system components:

    • API Definition: Users defining OAuth2-protected APIs will use the new configuration structure for PRM and can now configure token exchange.
    • Gateway (EE): The request flow for applicable OAuth2 APIs is altered to include an external call to an IdP, which may affect latency and error handling.
    • Gateway (OSS): The feature is disabled via a no-op middleware shim.

Token Exchange Middleware Flow

sequenceDiagram
    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
Loading

PRM Precedence Logic

graph 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;
Loading

Scope Discovery & Context Expansion

  • The PR title and description focus on the PRM refactoring, but the diff reveals a much larger scope: the implementation of the entire Token Exchange feature. This is a significant addition that constitutes the majority of the code changes and architectural impact.
  • This PR is part of a larger epic to enhance OAuth2 capabilities. The introduction of a new, EE-gated middleware establishes a pattern that may be used for future enterprise features.
  • The changes in gateway/api_loader.go show that the new OAuth2ExchangeMiddleware is injected into the middleware chain immediately after the existing OAuth2Middleware, confirming the architectural flow where the former acts as a setup step for the latter.
  • The tests in gateway/mw_oauth2_gating_test.go are crucial as they explicitly define and validate the behavioral differences between the OSS and EE builds, ensuring that the feature is correctly disabled in the open-source version.
Metadata
  • Review Effort: 5 / 5
  • Primary Label: feature

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 /visor ask <your question>

@probelabs

probelabs Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Security Issues (2)

Severity Location Issue
🔴 Critical gateway/mw_oauth2.go:90-76
The OAuth2 middleware uses claims from a JWT for scope-based authorization checks without verifying the token's signature. The `parseBearerClaims` function calls `oauth2common.ParseUnverifiedClaims`, and the resulting claims are used in `tokenSatisfiesAnyAlternative` to make an authorization decision. This allows an attacker to forge a JWT with arbitrary claims (e.g., any scopes) and bypass the scope check, as long as the token is well-formed. An attacker could use a token with the algorithm set to `none` to pass parsing and then include any scopes required to access a protected resource.
💡 SuggestionBefore using any claims from the JWT for authorization decisions, the token's signature must be cryptographically verified against the configured public keys (e.g., from a JWKS endpoint). The middleware should be configured with the necessary parameters for validation, such as the JWKS URL, expected issuer, and audience, similar to the existing JWT validation middleware.
🟡 Warning apidef/oas/oauth2.go:315-320
The validation for the `tokenEndpoint` URL in `validateExchangeTokenEndpoint` correctly restricts the scheme to `http` or `https`, which prevents Server-Side Request Forgery (SSRF) attacks using schemes like `file://` or `gopher://`. However, it explicitly does not restrict requests to private or loopback IP addresses. While the comment notes this is intentional because the API definition is admin-controlled, it still presents a risk. An administrator could inadvertently configure a URL pointing to an internal service not meant to be exposed, or an attacker with limited access to the management plane could leverage this to pivot into the internal network.
💡 SuggestionConsider adding an option to enforce egress controls on the `tokenEndpoint` URL, such as a configurable denylist or allowlist for IP ranges. This could be an opt-in feature (e.g., `enforce_private_ip_block: true`) to maintain backward compatibility for users who rely on accessing internal IdPs, while providing an additional layer of security for those who do not.

Security Issues (2)

Severity Location Issue
🔴 Critical gateway/mw_oauth2.go:90-76
The OAuth2 middleware uses claims from a JWT for scope-based authorization checks without verifying the token's signature. The `parseBearerClaims` function calls `oauth2common.ParseUnverifiedClaims`, and the resulting claims are used in `tokenSatisfiesAnyAlternative` to make an authorization decision. This allows an attacker to forge a JWT with arbitrary claims (e.g., any scopes) and bypass the scope check, as long as the token is well-formed. An attacker could use a token with the algorithm set to `none` to pass parsing and then include any scopes required to access a protected resource.
💡 SuggestionBefore using any claims from the JWT for authorization decisions, the token's signature must be cryptographically verified against the configured public keys (e.g., from a JWKS endpoint). The middleware should be configured with the necessary parameters for validation, such as the JWKS URL, expected issuer, and audience, similar to the existing JWT validation middleware.
🟡 Warning apidef/oas/oauth2.go:315-320
The validation for the `tokenEndpoint` URL in `validateExchangeTokenEndpoint` correctly restricts the scheme to `http` or `https`, which prevents Server-Side Request Forgery (SSRF) attacks using schemes like `file://` or `gopher://`. However, it explicitly does not restrict requests to private or loopback IP addresses. While the comment notes this is intentional because the API definition is admin-controlled, it still presents a risk. An administrator could inadvertently configure a URL pointing to an internal service not meant to be exposed, or an attacker with limited access to the management plane could leverage this to pivot into the internal network.
💡 SuggestionConsider adding an option to enforce egress controls on the `tokenEndpoint` URL, such as a configurable denylist or allowlist for IP ranges. This could be an opt-in feature (e.g., `enforce_private_ip_block: true`) to maintain backward compatibility for users who rely on accessing internal IdPs, while providing an additional layer of security for those who do not.
\n\n ### Architecture Issues (2)
Severity Location Issue
🟡 Warning gateway/api_loader.go:527
The `OAuth2TokenExchangeMiddleware` (initialized by `getOAuth2ExchangeMw`) has an implicit dependency on `OAuth2Middleware`. It relies on `OAuth2Middleware` running first to parse the inbound token and place an `oauth2common.State` object into the request's context. This dependency is not enforced by the compiler and depends entirely on the correct manual ordering of middleware in this file.
💡 SuggestionTo make this dependency explicit and prevent future maintenance issues from accidental reordering, add a comment explaining the required order. For example: `// getOAuth2ExchangeMw must be added after OAuth2Middleware as it depends on the context state set by it.` Additionally, the `OAuth2TokenExchangeMiddleware` could be made more robust by checking for the presence of the state and logging a clear error or panicking during startup/testing if it's missing, making the dependency fail-fast.
🟡 Warning gateway/mw_oauth2.go:64-128
The `OAuth2Middleware.ProcessRequest` function has grown in complexity and now serves multiple purposes: it gates requests based on whether scope-check or token-exchange is active, it parses the bearer token, it performs the scope check, and it prepares a context `State` object for the downstream token-exchange middleware. This mixing of concerns (validation and data preparation for another component) slightly reduces the clarity of its primary role.
💡 SuggestionConsider refactoring the token parsing and state preparation logic into a separate, dedicated middleware that runs before both `OAuth2Middleware` (for scope checks) and `OAuth2TokenExchangeMiddleware`. This new middleware's sole responsibility would be to process the `Authorization` header and populate the context if a valid token is found. This would improve separation of concerns, making each middleware's role more focused and the data flow more explicit. However, the current approach of using `OAuth2Middleware` as a multi-purpose pre-processor for all OAuth2 features is a pragmatic and acceptable pattern for extensibility, so this is a suggestion for future improvement rather than a required change.

Performance Issues (2)

Severity Location Issue
🟡 Warning ee/middleware/oauth2tokenexchange/exchange.go:264
A new HTTP client appears to be created for each token exchange request within `exchangeAtIdP`. This function is called on every cache miss. Instantiating `http.Client` per request is a common performance anti-pattern that bypasses connection pooling, which can lead to socket exhaustion (many sockets in `TIME_WAIT` state) and increased latency under load.
💡 SuggestionUse a single, shared `http.Client` instance for all outgoing IdP requests to leverage connection pooling. Per-request or per-provider timeouts can be effectively managed by creating a derived context with `context.WithTimeout` for each `http.Request`, which is then passed to the client's `Do` method. This avoids the overhead and resource issues of creating new clients.
🟡 Warning ee/middleware/oauth2tokenexchange/exchange.go:55-56
The function `subjectIDFromState` falls back to calculating a SHA256 hash of the entire raw token to generate a cache key component if the `sub` (subject) claim is missing. For large JWTs, performing a cryptographic hash on every request that results in a cache lookup can lead to high CPU utilization and become a performance bottleneck, especially under high traffic.
💡 SuggestionTo ensure optimal performance, the documentation for this feature should strongly recommend that inbound tokens contain a `sub` claim. Additionally, consider adding a log warning (that fires once or is throttled) when the system frequently encounters tokens without a `sub` claim, to alert operators to this potential performance issue in their environment.

Quality Issues (1)

Severity Location Issue
🟡 Warning gateway/api_definition.go:776-787
The change to `loader.ReadFromURIFunc` to handle secret replacement in local OAS files is not related to the main features of this pull request (OAuth2 PRM and Token Exchange). Including unrelated changes in a PR makes it harder to review and understand, and can complicate future debugging or rollbacks. This change should ideally be in a separate, dedicated pull request.
💡 SuggestionMove this change to a separate pull request with a clear title and description explaining the need for secret replacement in OAS files loaded from disk.

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 /visor ask <your question>

@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from 361a9a3 to e4ee669 Compare May 12, 2026 19:39
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from 897b39b to 39e200e Compare May 13, 2026 12:04
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from e4ee669 to dda4236 Compare May 13, 2026 12:05
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from 39e200e to 840f356 Compare May 13, 2026 14:29
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from dda4236 to a6ed678 Compare May 13, 2026 14:30
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from 840f356 to f79b58e Compare May 14, 2026 08:27
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from a6ed678 to 13a959e Compare May 14, 2026 08:27
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from f79b58e to f3bdd76 Compare May 15, 2026 07:01
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from 13a959e to c26dc11 Compare May 15, 2026 07:01
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from f3bdd76 to 3453698 Compare May 15, 2026 07:04
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from c26dc11 to 5e8d053 Compare May 15, 2026 07:04
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from 3453698 to 09a4f6b Compare May 18, 2026 16:16
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from 5e8d053 to 134f267 Compare May 18, 2026 16:34
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch 6 times, most recently from f82313a to b60992c Compare May 20, 2026 12:50
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch 4 times, most recently from 0ff24c9 to 11b3da6 Compare May 22, 2026 12:21

@edsonmichaque edsonmichaque left a comment

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.

LGTM

@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from 11b3da6 to e2c1512 Compare May 25, 2026 06:04
lghiur and others added 2 commits May 27, 2026 21:06
… 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]>
@lghiur
lghiur force-pushed the TT-17174-scope-check-per-operation branch from 7b49eee to 1d0d8ad Compare May 27, 2026 18:06
…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]>
@lghiur
lghiur force-pushed the TT-17175-prm-new-structure branch from e2c1512 to 79c1105 Compare May 27, 2026 18:07
Base automatically changed from TT-17174-scope-check-per-operation to master June 3, 2026 15:26
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🎯 Recommended Merge Targets

Based on JIRA ticket TT-17175: Protected Resource Metadata — new structure inside oauth2

Fix Version: Tyk 5.14.0

⚠️ Warning: Expected release branches not found in repository

Required:

  • master - No matching release branches found. Fix will be included in future releases.

📋 Workflow

  1. Merge this PR to master first

lghiur and others added 10 commits June 8, 2026 13:47
…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]>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🚨 Jira Linter Failed

Commit: c8c412f
Failed at: 2026-06-08 22:05:13 UTC

The Jira linter failed to validate your PR. Please check the error details below:

🔍 Click to view error details
failed to get Jira issue: failed to fetch Jira issue TT-17175: Issue does not exist or you do not have permission to see it.: request failed. Please analyze the request body for more details. Status code: 404

Next Steps

  • Ensure your branch name contains a valid Jira ticket ID (e.g., ABC-123)
  • Verify your PR title matches the branch's Jira ticket ID
  • Check that the Jira ticket exists and is accessible

This comment will be automatically deleted once the linter passes.

@sonarqubecloud

sonarqubecloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
81.6% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@lghiur
lghiur merged commit c8d9455 into master Jun 9, 2026
141 of 160 checks passed
@lghiur
lghiur deleted the TT-17175-prm-new-structure branch June 9, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants