Skip to content

[TT-17172] OAuth2 security scheme: foundation container#8198

Merged
lghiur merged 1 commit into
masterfrom
TT-17172-oauth2-foundation
May 15, 2026
Merged

[TT-17172] OAuth2 security scheme: foundation container#8198
lghiur merged 1 commit into
masterfrom
TT-17172-oauth2-foundation

Conversation

@lghiur

@lghiur lghiur commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces the new-style oauth2 security scheme as an empty container. Operators can declare and round-trip the scheme; the sub-blocks (scope check, protected-resource metadata, token exchange, introspection) plug into this container in follow-up work.

Apidef: OAuth2 struct (master Enabled + standard AuthSources), HasContent / IsEmpty, fillOAuth2 / fillOAuth2OASScheme, GetTykOAuth2Config / IsOAuth2Scheme helpers, schema deltas in 4 files (oas, oas-strict, mcp, streams). Gateway: APISpec.GetOAuth2Config returns one configured oauth2 scheme on an OAS API.

Duplicate oauth2 schemes on the same API are not validated — GetOAuth2Config returns the first match from map iteration. This matches the behaviour of every other auth scheme on the SecuritySchemes map (JWT, Token, HMAC, Basic, OIDC, ExternalOAuth); there is no per-scheme uniqueness validator anywhere in apidef/oas for those schemes either.

Story

Companion PRs

GW-only — this work ships to tyk only. Sub-blocks land in TT-17173 (#8199, stacked on this branch, with companions TykTechnologies/tyk-analytics#5629 and TykTechnologies/tyk-analytics-ui#4370) and beyond.

Test plan

  • go test ./apidef/oas/ ./gateway/ clean — HasContent / IsEmpty / fill round-trip / IsOAuth2Scheme / TestGetOAuth2Config_AcceptsDuplicatesAndReturnsOne (pins that duplicate oauth2 schemes are accepted at load time and GetOAuth2Config returns one)
  • Manual smoke

Risk

Schema deltas in 4 files (oas, oas-strict, mcp, streams) — they mirror each other and must stay in sync; drift is a silent-UI-breakage class of bug. The map-probe disambiguator is intentionally absent at this stage and is introduced when the first sub-block lands.

🤖 Generated with Claude Code

Ticket Details

TT-17172
Status In Code Review
Summary Introduce new oauth2 security scheme (foundation)

Generated at: 2026-05-14 08:07:27

@probelabs

probelabs Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the foundational container for a new-style OAuth2 security scheme. It allows operators to declare the scheme in an API definition, enabling the gateway to parse, validate, and round-trip the configuration. This initial implementation is a non-functional skeleton; the logic for token validation, scope checks, and other OAuth2 flows will be added in subsequent work.

Files Changed Analysis

The core logic is introduced in two new files, apidef/oas/oauth2.go and its corresponding test file apidef/oas/oauth2_test.go, which define the OAuth2 struct and its associated helpers. A key pattern is the parallel modification of four schema files (apidef/mcp/schema/x-tyk-api-gateway.json, apidef/oas/schema/x-tyk-api-gateway.json, apidef/oas/schema/x-tyk-api-gateway.strict.json, and apidef/streams/schema/x-tyk-api-gateway.json) to add the X-Tyk-OAuth2 definition, ensuring consistency. The new functionality is integrated into the gateway's existing security processing via a call to fillOAuth2 in apidef/oas/security.go. A new helper function, GetOAuth2Config, is added to gateway/model_apispec.go to access the new scheme's configuration.

Architecture & Impact Assessment

  • What this PR accomplishes: It establishes the data structures and schema definitions for a new OAuth2 security scheme. This allows the gateway to correctly parse, validate, and round-trip API definitions that include this scheme, setting the stage for future implementation of functional OAuth2 features.

  • Key technical changes introduced:

    • A new apidef.oas.OAuth2 struct is defined as the container for the scheme's configuration, initially containing only a master Enabled toggle and standard AuthSources.
    • The fillSecurity process is updated to call fillOAuth2, which materializes a placeholder entry for the enabled scheme in the public OAS document's components.securitySchemes.
    • Helper functions like GetTykOAuth2Config and IsOAuth2Scheme are added to identify and retrieve the new scheme's configuration.
    • The X-Tyk-OAuth2 definition is added to the API definition JSON schemas across MCP, OAS, and Streams formats.
  • Affected system components:

    • API Definition (apidef): The core package for API definitions is extended to support the new scheme.
    • Gateway (gateway): The gateway's API loading and configuration handling logic is updated for the new scheme.
    • API Definition Schemas: Any system that relies on these schemas for validation or UI generation (like the Tyk Dashboard) will be impacted by the new definition.

OAS Generation Flow for Security Schemes

graph TD
    A[API Load] --> B(fillSecurity);
    B --> C(fillBasic);
    B --> D(fillOAuth);
    B --> E(fillExternalOAuth);
    B --> F(fillOAuth2); 
    F --> G{Scheme Enabled?};
    G -- Yes --> H(Add to OAS Components);
    G -- No --> I(Do not advertise in OAS);

    style F fill:#cde4ff,stroke:#6495ED,stroke-width:2px
Loading

Scope Discovery & Context Expansion

The changes in this PR are confined to the API definition structure and the gateway's configuration loading process. There is no immediate impact on runtime request processing or traffic management, as the introduced scheme is a non-functional container. The primary risk, as noted by the author, is the potential for drift between the four duplicated schema definitions, which must be kept in sync manually.

A key architectural decision, confirmed by the test TestGetOAuth2Config_AcceptsDuplicatesAndReturnsOne, is to permit multiple oauth2 schemes in a single API definition. The GetOAuth2Config function will return the first one it encounters during map iteration, which is non-deterministic. This matches the behavior of other security schemes but could lead to unpredictable behavior if not handled carefully in future work.

This work is the first step in a larger initiative to implement a comprehensive, new-style OAuth2 security provider. Subsequent PRs will build upon this foundation by adding configuration and logic for features like token introspection, scope validation, and token exchange. These future changes will populate the oas.OAuth2 struct with functional settings and introduce the corresponding runtime enforcement logic in the gateway.

Metadata
  • Review Effort: 2 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-05-14T08:09:50.551Z | Triggered by: pr_updated | Commit: f126d68

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs

probelabs Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Security Issues (2)

Severity Location Issue
🟡 Warning gateway/model_apispec.go:194-204
When multiple OAuth2 security schemes are defined for an API, the function `GetOAuth2Config` returns a non-deterministically chosen scheme due to iterating over a map. This could lead to an unintended or less secure configuration being applied if an administrator mistakenly configures multiple schemes. While this matches the behavior of other authentication schemes, it presents a potential security risk.
💡 SuggestionTo prevent ambiguity and ensure predictable behavior, validate that only one OAuth2 security scheme is defined per API. If multiple schemes are found, either return an error or log a prominent warning indicating which scheme is being used and that others are being ignored. A validation check during API definition loading would be the most robust solution.
🟡 Warning apidef/oas/oauth2.go:94-96
The function `fillOAuth2OASScheme` populates the OAS document with placeholder URLs (`/oauth/authorize`, `/oauth/token`) for the OAuth2 flow. These paths might not correspond to actual, implemented endpoints in the gateway, potentially misleading API consumers or security scanners into believing these endpoints exist and are functional. This could lead to confusion or false positives in security scans.
💡 SuggestionConsider adding a comment in the generated OAS specification next to the placeholder URLs to clarify that they are placeholders and will be overridden by functional endpoints when configured. Alternatively, ensure that documentation clearly states the purpose of these placeholder values and that they are not active endpoints by default.

Architecture Issues (2)

Severity Location Issue
🟡 Warning gateway/model_apispec.go:197-208
The function `GetOAuth2Config` returns only the first `oauth2` security scheme found during map iteration, making the selection non-deterministic if multiple `oauth2` schemes are defined. This is consistent with how other security schemes are handled but propagates a design that can lead to unpredictable behavior and user confusion, as the gateway will silently ignore all but one scheme.
💡 SuggestionTo prevent misconfiguration, consider adding a validation step to the API definition loading process that either warns or errors if more than one `oauth2` security scheme is defined. Alternatively, if multiple schemes are a valid use case, the logic should be updated to handle all of them explicitly rather than selecting one arbitrarily.
🟡 Warning apidef/mcp/schema/x-tyk-api-gateway.json:1312-1315
The `X-Tyk-OAuth2` definition is duplicated across four schema files (`apidef/mcp/schema`, `apidef/oas/schema`, `apidef/oas/schema.strict`, `apidef/streams/schema`). This follows an existing pattern in the codebase but increases the maintenance burden and creates a risk of schemas drifting out of sync over time.
💡 SuggestionAs a long-term architectural improvement, consider refactoring to use a single source of truth for these shared schema definitions, from which the different schema files can be generated. This would reduce duplication and prevent inconsistencies.

✅ Performance Check Passed

No performance issues found – changes LGTM.

Security Issues (2)

Severity Location Issue
🟡 Warning gateway/model_apispec.go:194-204
When multiple OAuth2 security schemes are defined for an API, the function `GetOAuth2Config` returns a non-deterministically chosen scheme due to iterating over a map. This could lead to an unintended or less secure configuration being applied if an administrator mistakenly configures multiple schemes. While this matches the behavior of other authentication schemes, it presents a potential security risk.
💡 SuggestionTo prevent ambiguity and ensure predictable behavior, validate that only one OAuth2 security scheme is defined per API. If multiple schemes are found, either return an error or log a prominent warning indicating which scheme is being used and that others are being ignored. A validation check during API definition loading would be the most robust solution.
🟡 Warning apidef/oas/oauth2.go:94-96
The function `fillOAuth2OASScheme` populates the OAS document with placeholder URLs (`/oauth/authorize`, `/oauth/token`) for the OAuth2 flow. These paths might not correspond to actual, implemented endpoints in the gateway, potentially misleading API consumers or security scanners into believing these endpoints exist and are functional. This could lead to confusion or false positives in security scans.
💡 SuggestionConsider adding a comment in the generated OAS specification next to the placeholder URLs to clarify that they are placeholders and will be overridden by functional endpoints when configured. Alternatively, ensure that documentation clearly states the purpose of these placeholder values and that they are not active endpoints by default.
\n\n ### Architecture Issues (2)
Severity Location Issue
🟡 Warning gateway/model_apispec.go:197-208
The function `GetOAuth2Config` returns only the first `oauth2` security scheme found during map iteration, making the selection non-deterministic if multiple `oauth2` schemes are defined. This is consistent with how other security schemes are handled but propagates a design that can lead to unpredictable behavior and user confusion, as the gateway will silently ignore all but one scheme.
💡 SuggestionTo prevent misconfiguration, consider adding a validation step to the API definition loading process that either warns or errors if more than one `oauth2` security scheme is defined. Alternatively, if multiple schemes are a valid use case, the logic should be updated to handle all of them explicitly rather than selecting one arbitrarily.
🟡 Warning apidef/mcp/schema/x-tyk-api-gateway.json:1312-1315
The `X-Tyk-OAuth2` definition is duplicated across four schema files (`apidef/mcp/schema`, `apidef/oas/schema`, `apidef/oas/schema.strict`, `apidef/streams/schema`). This follows an existing pattern in the codebase but increases the maintenance burden and creates a risk of schemas drifting out of sync over time.
💡 SuggestionAs a long-term architectural improvement, consider refactoring to use a single source of truth for these shared schema definitions, from which the different schema files can be generated. This would reduce duplication and prevent inconsistencies.
\n\n \n\n

Quality Issues (3)

Severity Location Issue
🟢 Info gateway/model_apispec_test.go:87-145
The test `TestAPISpec_GetOAuth2Config` is implemented as a series of distinct sub-tests for different scenarios. While functional, this pattern can be made more concise and idiomatic for Go by refactoring it into a single table-driven test.
💡 SuggestionConsider refactoring this test to use a slice of test case structs. Each struct would contain the test name, the input `APISpec`, and the expected outputs. This consolidates the test logic, improves readability, and makes it easier to add new test cases in the future.
🟡 Warning apidef/oas/oauth2_test.go:128-129
The test `TestOAuth2_RoundTripJSONPreservesEnabled` validates the marshaled JSON output using a raw string comparison (`assert.Contains`). This is a brittle testing practice because it can fail on non-functional changes, such as different key ordering in the JSON output or changes in whitespace, making the test fragile.
💡 SuggestionTo make the test more robust, unmarshal the JSON into a generic `map[string]interface{}` and programmatically inspect the map's structure and values. This approach is resilient to formatting changes and more precisely tests the data structure. For example: ```go var data map[string]interface{} require.NoError(t, json.Unmarshal(raw, &data))

xtyk, ok := data["x-tyk-api-gateway"].(map[string]interface{})
require.True(t, ok, "x-tyk-api-gateway extension should exist")
// ... traverse the map and assert on the specific values.

</details>
</div></td>
    </tr>
    <tr>
      <td>🟡 Warning</td>
      <td><code>apidef/mcp/schema/x-tyk-api-gateway.json:1961-1971</code></td>
      <td><div>The addition of the `X-Tyk-OAuth2` definition to four separate schema files (`apidef/mcp/schema/x-tyk-api-gateway.json`, `apidef/oas/schema/x-tyk-api-gateway.json`, `apidef/oas/schema/x-tyk-api-gateway.strict.json`, `apidef/streams/schema/x-tyk-api-gateway.json`) increases maintenance overhead. These files must be kept in sync manually, which is error-prone and risks them drifting apart over time.        <details><summary>💡 <strong>Suggestion</strong></summary>To improve maintainability and reduce duplication, consider generating these schema files from a single source of truth. A script could take a base schema definition and apply the necessary variations for each target format (e.g., adding `additionalProperties: false` for the strict schema).
</details>
</div></td>
    </tr>
  </tbody>
</table>
<!-- visor:section-end id="quality" -->

<!-- visor:thread-end key="TykTechnologies/tyk#8198@f126d68" -->

---

*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*

*Last updated: 2026-05-14T08:09:22.549Z | Triggered by: pr_updated | Commit: f126d68*

💡 **TIP:** You can chat with Visor using `/visor ask <your question>`
<!-- /visor-comment-id:visor-thread-review-TykTechnologies/tyk#8198 -->

@lghiur
lghiur force-pushed the TT-17172-oauth2-foundation branch 3 times, most recently from baf9ceb to 7f92ebd Compare May 13, 2026 14:19
Comment thread apidef/oas/oauth2.go
Comment thread gateway/model_apispec.go
@edsonmichaque
edsonmichaque self-requested a review May 14, 2026 07:52

@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

Adds the new-style oauth2 security scheme as an empty container that
operators can declare and round-trip through the gateway. The scheme
exposes a master `Enabled` toggle and inherits the standard
`AuthSources` contract; subsequent feature work plugs additional
sub-blocks into this container.

Apidef
- OAuth2 struct with Enabled + AuthSources, stored under
  x-tyk-api-gateway.server.authentication.securitySchemes[name].
- HasContent / IsEmpty predicates.
- fillOAuth2 / fillOAuth2OASScheme materialise pre-typed *OAuth2
  entries into the public OAS document with a typeOAuth2 entry under
  Components.SecuritySchemes.
- GetTykOAuth2Config / IsOAuth2Scheme helpers.
- Schema deltas in apidef/{oas, oas-strict, mcp, streams}/schema/
  x-tyk-api-gateway.json: add the X-Tyk-OAuth2 definition (enabled +
  AuthSource refs) and reference it from the securitySchemes
  pattern-properties anyOf list.
- security.go: add *OAuth2 to isProprietarySchemeType and call
  s.fillOAuth2() from fillSecurity after the existing scheme fillers.

Gateway
- APISpec.GetOAuth2Config returns one configured oauth2 scheme on an
  OAS API; duplicates are accepted (map-iteration first match),
  matching the behaviour of every other auth scheme on this map
  (JWT, Token, HMAC, Basic, OIDC, ExternalOAuth).

Tests
- apidef/oas: HasContent / IsEmpty for nil, zero, and enabled
  receivers; fill adds OAS Components entry for enabled schemes;
  fill skips disabled schemes from OAS Components; JSON round-trip
  preserves enabled; IsOAuth2Scheme lookup.
- gateway: GetOAuth2Config returns one of the configured schemes
  when duplicates are present and APISpec.Validate accepts the
  doc at load time.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@lghiur
lghiur force-pushed the TT-17172-oauth2-foundation branch from da95b9b to f126d68 Compare May 14, 2026 08:06
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

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

See analysis details on SonarQube Cloud

@lghiur
lghiur merged commit bc1311b into master May 15, 2026
151 of 160 checks passed
@lghiur
lghiur deleted the TT-17172-oauth2-foundation branch May 15, 2026 06:28
lghiur added a commit that referenced this pull request May 27, 2026
## Summary

Adds the OAS-native OAuth 2.0 `scopeCheck` sub-block on the new `oauth2`
security scheme and the gateway middleware that enforces it. Required
scopes live in the **OAS root `security:` array** — standard OpenAPI
Security Requirement Objects, OR-of-AND across entries. The `scopeCheck`
block on the Tyk extension carries only the token-side knobs: which JWT
claims to read scopes from, how to split string-form values, and which
scope-source mode is active. Validation, scope merging across claims,
and the RFC 6750 §3 `Bearer SP 1#auth-param` failure challenge all live
here.

The scheme is name-agnostic — operators can name the OAS
`securitySchemes` entry anything (`oauth2`, `prctOauth`, `corpOAuth`,
…); enforcement uses whatever name the document declares.

## Story

- JIRA: https://tyktech.atlassian.net/browse/TT-17173

## Stacked on

This PR is stacked on TT-17172 — #8198. Review/merge
that first; once it merges this can be retargeted to `master`.

## Companion PRs

These three ship together — please block until all three are green:

- `tyk`: this PR
- `tyk-analytics`: TykTechnologies/tyk-analytics#5629
- `tyk-analytics-ui`: TykTechnologies/tyk-analytics-ui#4370

## Behaviour

### Apidef (`apidef/oas/`)

- `OAuth2ScopeCheck`: `Enabled`, `ClaimNames []string`, `Separator
string`, `ScopeSource string`. There is no `Scopes` field — scope policy
lives in OAS root `security:`.
- `ScopeSource` constants: `operation`, `global`, `union` (default).
- Map-probe disambiguator (`asOAuth2Scheme` + `mapHasOAuth2SubBlock`)
tells the OAS-native `OAuth2` scheme apart from legacy `*OAuth` /
`*ExternalOAuth` schemes after JSON round-trip — recognised by presence
of an oauth2 sub-block key.
- `SecurityRequirementScopes [][]map[string][]string` sibling field on
`apidef.APIDefinition` preserves per-scheme scope arrays across the
extract/fill round-trip (omitempty; forward-compatible).
- `extractSecurityTo` single-alternative branch: populates
`SecurityRequirements` with every scheme in the requirement (including
empty-scope ones like `jwtAuth: []`) so multi-scheme single-AND security
policies (JWT + OAuth2 in one entry) survive round-trip without silently
dropping the JWT scheme.
- Validator rejects an unknown `scopeSource` at API load with a clear
error.
- Strict + non-strict OAS schemas declare `X-Tyk-OAuth2-ScopeCheck` with
the four token-side fields.

### Gateway (`gateway/mw_oauth2.go`)

- `OAuth2Middleware` enforces scope check when `oauth2.enabled` and
`scopeCheck.enabled` are both true.
- `rootSecurityAlternatives(security, schemeName, scopeCheck)` reads
root `security:` and returns the OR-of-AND list of alternatives drawn
from entries that reference the configured scheme. Declared order is
preserved end-to-end so the operator sees their authored form on the
failure challenge.
- Token's scope set is the **merged union** across every claim in
`ClaimNames` (default `["scope", "scp"]`). Each claim value is parsed by
`Separator` (default single space), JSON array form, or comma-separated
form per RFC 6749 §3.3.
- On insufficient scope: 403, RFC 6750 §3 `WWW-Authenticate: Bearer
error="insufficient_scope" …` challenge citing the **first** declared
alternative only (intentionally — listing every alternative would leak
that the API has a service-account path and a user path), JSON body with
`error` / `error_description` / `scope`, and an `OAuth2ScopeCheckFailed`
audit event carrying the cited alternative + full alternative list.
- An entry that lists the scheme with an empty scope list (`security: -
oauth2: []`) is treated as "auth required, no specific scope" —
vacuously satisfied, short-circuits the OR to pass.

### Wire-protocol constants

- `OAuth2ErrInsufficientScope` = `"insufficient_scope"` (RFC 6750 §3.1)
- `OAuth2ErrInvalidToken` = `"invalid_token"` (RFC 6750 §3.1)

## Test plan

- [x] `go test ./apidef/oas/ ./gateway/ ./internal/oauth2common/` — unit
tests cover: nil/empty/operation-suppression, single-AND, OR-across,
OR-of-AND, sort+dedup, vacuous-satisfaction, multi-scheme
single-alternative round-trip preservation, name-agnostic enforcement
(`TestOAuth2Middleware_NameAgnostic`), RFC 6750 §3 challenge header form
- [x] Python e2e — runs in the dashboard companion PR
- [ ] Postman collection via newman
- [ ] Manual smoke

## Risk

Failure-challenge form cites only the first declared alternative —
deliberate, regression-tested. The strict-schema deltas across four
schema files must stay in sync. The map-probe disambiguator's sub-block
key list (`oauth2SubBlockKeys`) needs an entry added whenever a new
OAuth2 sub-block is introduced, otherwise the typed view is silently
dropped on JSON round-trip.

🤖 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-17173" title="TT-17173"
target="_blank">TT-17173</a>
</summary>

|         |    |
|---------|----|
| Status  | Merge |
| Summary | OAuth2 scope check — global / API level |

Generated at: 2026-05-26 15:20:57

</details>

<!---TykTechnologies/jira-linter ends here-->

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
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.

3 participants