Skip to content

[TT-17173] OAuth2 scope check (API-level / global)#8199

Merged
lghiur merged 2 commits into
masterfrom
TT-17173-scope-check-api-level
May 27, 2026
Merged

[TT-17173] OAuth2 scope check (API-level / global)#8199
lghiur merged 2 commits into
masterfrom
TT-17173-scope-check-api-level

Conversation

@lghiur

@lghiur lghiur commented May 11, 2026

Copy link
Copy Markdown
Collaborator

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

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

  • 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
  • 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

Ticket Details

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

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

@probelabs

probelabs Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

This PR introduces an OAS-native OAuth 2.0 scope-checking middleware to the Tyk Gateway. This feature enables API-level or global scope enforcement based on JWT claims. The required scopes are defined in the standard OAS security array, supporting OR-of-AND logic. The configuration, located in a new scopeCheck block within the x-tyk-api-gateway extension, specifies which JWT claims to read scopes from, how to parse them, and the enforcement strategy (global, per-operation, or a union). In case of insufficient scope, the middleware returns a 403 Forbidden response with a standard RFC 6750 WWW-Authenticate challenge header.

Files Changed Analysis

This PR adds a significant new feature, reflected in 17 changed files with 2372 additions and 63 deletions. The core logic is encapsulated in new files:

  • gateway/mw_oauth2.go: The new middleware that performs the scope check.
  • gateway/mw_oauth2_test.go: Extensive unit and end-to-end tests for the middleware.
  • internal/oauth2common/: A new package with helpers for parsing JWT claims without signature verification.

Configuration and data structure changes are primarily in:

  • apidef/oas/oauth2.go: Defines the new OAuth2 and OAuth2ScopeCheck configuration structs.
  • apidef/api_definitions.go: Adds the SecurityRequirementScopes field to persist scope policies from the OAS definition.
  • apidef/oas/security.go: Integrates the new scope persistence logic into the OAS extract/fill cycle.

The new middleware is wired into the request processing chain in gateway/api_loader.go, and a new OAuth2ScopeCheckFailed event is added in internal/event/event.go for auditing purposes.

Architecture & Impact Assessment

  • What this PR accomplishes: It implements a flexible, OAS-native OAuth2 scope enforcement feature directly within the Tyk Gateway. This allows API owners to define fine-grained authorization rules based on JWT scopes as part of the API's contract.

  • Key technical changes introduced:

    1. New Middleware: A new gateway.OAuth2Middleware is added to the API processing chain to handle scope validation.
    2. OAS-Native Configuration: The feature is configured within the x-tyk-api-gateway extension of an OAS definition, making it native to the API's contract.
    3. Scope Persistence: A new SecurityRequirementScopes field is added to the APIDefinition to ensure scope policies survive the round-trip between the OAS document and Tyk's internal representation.
    4. Standard-Compliant Error Handling: Uses RFC 6750 WWW-Authenticate headers for authorization failures, improving client interoperability.
  • Affected system components:

    • Gateway: The request processing pipeline is directly impacted by the addition of the new middleware.
    • API Definition: The structure for defining APIs is extended, and the system for loading and processing OAS definitions is updated.
    • Auditing & Analytics: A new OAuth2ScopeCheckFailed event is introduced for logging and analytics.

Request Flow Diagram

sequenceDiagram
    participant Client
    participant Tyk Gateway
    participant "Auth Middleware (e.g., JWT)"
    participant "OAuth2Middleware (New)"
    participant Upstream

    Client->>+Tyk Gateway: Request with Bearer Token
    Tyk Gateway->>+"Auth Middleware (e.g., JWT)": Verify Token Signature
    "Auth Middleware (e.g., JWT)"-->>-Tyk Gateway: Authentication OK
    Tyk Gateway->>+"OAuth2Middleware (New)": Check Scopes
    "OAuth2Middleware (New)"->>"OAuth2Middleware (New)": Parse claims from token (unverified)
    "OAuth2Middleware (New)"->>"OAuth2Middleware (New)": Compare token scopes to required scopes
    alt Scopes Sufficient
        "OAuth2Middleware (New)"-->>-Tyk Gateway: Authorization OK
        Tyk Gateway->>+Upstream: Forward Request
        Upstream-->>-Tyk Gateway: Response
        Tyk Gateway-->>-Client: Response
    else Scopes Insufficient
        "OAuth2Middleware (New)"-->>-Tyk Gateway: 403 Forbidden
        Tyk Gateway-->>-Client: 403 with WWW-Authenticate header
    end
Loading

Scope Discovery & Context Expansion

  • This PR is a foundational component of a larger OAuth2 initiative, as indicated by the stacked PR structure and references to companion PRs for tyk-analytics and tyk-analytics-ui. The changes here will enable UI-driven configuration and analytics reporting for this feature in other Tyk components.

  • Critical Security Consideration: The OAuth2Middleware performs authorization based on claims from a JWT, but it does not perform authentication. It uses oauth2common.ParseUnverifiedClaims, which explicitly does not verify the token's signature. The security of this feature relies entirely on a preceding authentication middleware (e.g., the standard JWT middleware) to have already authenticated the token. The middleware execution order is critical; if this middleware were to run on a request authenticated by a different mechanism (e.g., an API key), an attacker could pass a forged JWT with arbitrary scopes and bypass authorization. Reviewers must confirm that the middleware execution order is guaranteed to be correct in all configurations and that this architectural dependency is acceptable.

References

  • gateway/mw_oauth2.go: Core implementation of the new OAuth2 scope check middleware.
  • apidef/oas/oauth2.go: Defines the OAuth2 and OAuth2ScopeCheck configuration structs for the OAS extension.
  • internal/oauth2common/claims.go: Contains the ParseUnverifiedClaims helper for extracting JWT claims without signature verification.
  • gateway/api_loader.go: The location where the new OAuth2Middleware is appended to the middleware chain.
  • apidef/api_definitions.go: Introduces SecurityRequirementScopes to preserve scope details through the API definition lifecycle.
Metadata
  • Review Effort: 4 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-05-26T15:22:31.171Z | Triggered by: pr_updated | Commit: af52069

💡 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 (1)

Severity Location Issue
🔴 Critical gateway/mw_oauth2.go:106
The OAuth2 scope check middleware makes authorization decisions based on claims from a JWT that it parses directly from the request header without verifying its signature. This allows an attacker to forge a JWT with arbitrary scopes. If this middleware is used in combination with a non-JWT authentication method (like API keys), an attacker can satisfy the primary authentication and then use the forged JWT to bypass scope-based authorization checks.
💡 SuggestionThe middleware must not use unverified claims for authorization. It should retrieve claims that have been verified by a preceding authentication middleware (e.g., `JWTAuthMiddleware`) from the request's session state (`session.MetaData["jwt_claims"]`). If no verified JWT claims are available in the session, it indicates that the request was not authenticated via JWT, and the scope check should not be performed based on a header that could be spoofed. The middleware should not parse the token from the header itself.

Architecture Issues (2)

Severity Location Issue
🟡 Warning apidef/oas/oauth2.go:150
The mechanism for distinguishing the new OAS-native OAuth2 scheme from legacy schemes during JSON round-trip relies on a hardcoded list of sub-block keys (`oauth2SubBlockKeys`). If a new sub-block (e.g., for token exchange) is added to the `OAuth2` struct in the future, this list must be manually updated. Forgetting to do so would cause the system to misidentify the scheme as a legacy one, silently dropping the new configuration. This creates a maintenance risk.
💡 SuggestionTo make this more robust and less prone to human error, consider an alternative disambiguation strategy. One option is to add an explicit version or type field to the `OAuth2` struct, like `"schemeVersion": "oas-native"`. This would provide a stable identifier that doesn't need to be updated every time a new feature sub-block is added, reducing the risk of silent configuration loss during round-trips.
🟡 Warning gateway/mw_oauth2.go:113
The middleware directly marshals a JSON error response map. This hard-codes the error structure within the middleware, making it inconsistent with how other parts of the gateway might handle errors and harder to customize. It also mixes the logic of error generation with the core authorization task.
💡 SuggestionAbstract the error response generation. A better approach would be to return a specific error type (e.g., `ErrInsufficientScope`) from `ProcessRequest`. A separate, dedicated error-handling middleware or a centralized error response function could then catch this error and be responsible for formatting the final HTTP response (headers and body). This improves separation of concerns and aligns with existing patterns where middlewares return errors that are handled later in the chain.

Security Issues (1)

Severity Location Issue
🔴 Critical gateway/mw_oauth2.go:106
The OAuth2 scope check middleware makes authorization decisions based on claims from a JWT that it parses directly from the request header without verifying its signature. This allows an attacker to forge a JWT with arbitrary scopes. If this middleware is used in combination with a non-JWT authentication method (like API keys), an attacker can satisfy the primary authentication and then use the forged JWT to bypass scope-based authorization checks.
💡 SuggestionThe middleware must not use unverified claims for authorization. It should retrieve claims that have been verified by a preceding authentication middleware (e.g., `JWTAuthMiddleware`) from the request's session state (`session.MetaData["jwt_claims"]`). If no verified JWT claims are available in the session, it indicates that the request was not authenticated via JWT, and the scope check should not be performed based on a header that could be spoofed. The middleware should not parse the token from the header itself.
\n\n ### Architecture Issues (2)
Severity Location Issue
🟡 Warning apidef/oas/oauth2.go:150
The mechanism for distinguishing the new OAS-native OAuth2 scheme from legacy schemes during JSON round-trip relies on a hardcoded list of sub-block keys (`oauth2SubBlockKeys`). If a new sub-block (e.g., for token exchange) is added to the `OAuth2` struct in the future, this list must be manually updated. Forgetting to do so would cause the system to misidentify the scheme as a legacy one, silently dropping the new configuration. This creates a maintenance risk.
💡 SuggestionTo make this more robust and less prone to human error, consider an alternative disambiguation strategy. One option is to add an explicit version or type field to the `OAuth2` struct, like `"schemeVersion": "oas-native"`. This would provide a stable identifier that doesn't need to be updated every time a new feature sub-block is added, reducing the risk of silent configuration loss during round-trips.
🟡 Warning gateway/mw_oauth2.go:113
The middleware directly marshals a JSON error response map. This hard-codes the error structure within the middleware, making it inconsistent with how other parts of the gateway might handle errors and harder to customize. It also mixes the logic of error generation with the core authorization task.
💡 SuggestionAbstract the error response generation. A better approach would be to return a specific error type (e.g., `ErrInsufficientScope`) from `ProcessRequest`. A separate, dedicated error-handling middleware or a centralized error response function could then catch this error and be responsible for formatting the final HTTP response (headers and body). This improves separation of concerns and aligns with existing patterns where middlewares return errors that are handled later in the chain.
\n\n ### Performance Issues (1)
Severity Location Issue
🟡 Warning gateway/mw_oauth2.go:102
The `OAuth2Middleware` parses the raw JWT from the Authorization header on every request to extract claims for scope checking. This is redundant if a preceding JWT authentication middleware has already parsed and validated the token. Processing the same token twice on the request hot path introduces unnecessary CPU overhead and can impact gateway latency under load.
💡 SuggestionThe authentication middleware (e.g., JWT middleware) should store the verified claims in the request context. The `OAuth2Middleware` should then be modified to first check the context for these claims. If found, it should use them directly, avoiding the need to parse the token string again. This would look something like `claims, ok := r.Context().Value(claimsContextKey).(jwt.MapClaims)`. If the claims are not in the context, it could fall back to the current behavior or return an error, depending on the desired security posture.

Quality Issues (2)

Severity Location Issue
🟡 Warning gateway/mw_oauth2.go:102
The middleware's security model relies on an implicit dependency on a preceding JWT validation middleware. It re-parses the raw token from the header using `oauth2common.ParseUnverifiedClaims` instead of consuming already-verified claims from the request context. This makes the security model fragile and dependent on correct middleware ordering, which is not enforced by the types or interfaces. A misconfiguration could lead to authorization decisions being made on unverified claims.
💡 SuggestionTo make the security model more robust and explicit, the JWT validation middleware should store the verified claims in the request context. This middleware should then be modified to read claims exclusively from the context. If verified claims are not present in the context, the request should be rejected. This change would eliminate the dependency on middleware ordering and prevent the possibility of acting on unverified data.
🟡 Warning apidef/oas/oauth2.go:150
The logic to differentiate the OAS-native `oauth2` scheme from legacy schemes (e.g., `*OAuth`) during JSON unmarshaling relies on a hardcoded list of sub-block keys in the `oauth2SubBlockKeys` variable. As noted in the code comments, if a new sub-block is added to the `OAuth2` struct in the future, this list must be manually updated. Forgetting this step would cause the scheme to be misidentified, and the new functionality would be silently ignored.
💡 SuggestionThis pattern is a potential source of future bugs. While the comment is good, consider if there's a more robust way to perform this type of disambiguation that doesn't rely on a manually maintained list. If not, the existing comment is a good warning, but the pattern itself introduces maintenance risk.

Powered by Visor from Probelabs

Last updated: 2026-05-26T15:22:03.732Z | Triggered by: pr_updated | Commit: af52069

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

@lghiur

lghiur commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

/review The OAuth2 middleware parses JWT claims using `oauth2common.ParseUnverifiedClaims` without verifying the token's signature. It then uses these unverified claims to make an authorization decision about scopes. This creates a critical vulnerability where an attacker can forge a JWT with arbitrary scopes (e.g., `{"scope": "admin"}`), sign it with their own key or use `alg: "none"`, and bypass scope-based access control. The middleware must only act on claims from a token whose signature has been cryptographically verified. 💡 Suggestion The `OAuth2Middleware` must not perform authorization checks on unverified claims. The JWT signature must be verified before this middleware runs. A potential solution is to ensure that a standard JWT validation middleware runs before `OAuth2Middleware` and stores the verified claims in the request context. `OAuth2Middleware` should then retrieve the claims from the context instead of parsing the raw token itself. If no verified claims are found in the context, it should reject the request. -> Oauth2 middleware is meant to work together with the JWT middleware which does the validattion

@probelabs

probelabs Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Powered by Visor from Probelabs

Last updated: 2026-05-11T21:17:50.755Z | Triggered by: lghiur | Commit: 2648662

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

@probelabs

probelabs Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor
\n\n \n\n \n\n

Powered by Visor from Probelabs

Last updated: 2026-05-11T21:17:51.668Z | Triggered by: lghiur | Commit: 2648662

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

@lghiur
lghiur force-pushed the TT-17172-oauth2-foundation branch from 6f4e929 to a0f59d5 Compare May 13, 2026 11:59
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch from 59534f8 to 4bccd2b Compare May 13, 2026 12:03
@lghiur
lghiur force-pushed the TT-17172-oauth2-foundation branch 2 times, most recently from baf9ceb to 7f92ebd Compare May 13, 2026 14:19
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch from 4bccd2b to 78cca80 Compare May 13, 2026 14:24
@lghiur
lghiur force-pushed the TT-17172-oauth2-foundation branch from da95b9b to f126d68 Compare May 14, 2026 08:06
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch from 78cca80 to f3ba544 Compare May 14, 2026 08:26
lghiur added a commit that referenced this pull request May 15, 2026
## 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

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

## 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

- [x] `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](https://claude.com/claude-code)

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

### Ticket Details

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17172" title="TT-17172"
target="_blank">TT-17172</a>
</summary>

|         |    |
|---------|----|
| Status  | In Code Review |
| Summary | Introduce new `oauth2` security scheme (foundation) |

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

</details>

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

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Base automatically changed from TT-17172-oauth2-foundation to master May 15, 2026 06:28
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch 2 times, most recently from f43707f to da17d5a Compare May 15, 2026 07:03
Comment thread internal/oauth2common/claims.go Fixed
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch 2 times, most recently from 54cb29a to 5bc606b Compare May 15, 2026 08:41
// not own and would duplicate the upstream verifier.
func ParseUnverifiedClaims(token string) (jwt.MapClaims, error) {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
parsed, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch 8 times, most recently from f624baf to 0a3f94b Compare May 19, 2026 13:15
Comment thread internal/oauth2common/doc.go Outdated
@@ -0,0 +1,8 @@
// Package oauth2common holds the pure-function helpers used by the

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.

I believe this file has no real use.

Adds the OAS-native OAuth 2.0 security scheme with global scope-check
enforcement wired into the gateway middleware chain. Required scopes
live in the OAS root `security:` array (standard OpenAPI Security
Requirement Object); the Tyk-extension scheme carries only the
token-side knobs (where to read scopes from the JWT, which claims to
merge, how to split string-form claims, which source modes drive
enforcement).

apidef
- New apidef.APIDefinition.SecurityRequirementScopes (sibling of
  SecurityRequirements) preserves per-scheme scope arrays through the
  OAS extract/fill cycle. Each entry in the slice is a
  `map[scheme][]scope` aligned by index with SecurityRequirements.
- Both SecurityRequirements and SecurityRequirementScopes intentionally
  carry `bson:"…"` (no `,omitempty`). Mongo's `$set` semantics with
  `omitempty` would drop a nil from the update payload and leave a
  prior stored value intact — an operator who removed all security
  from an API would then see the deleted policy resurface on the next
  reload. The non-omitempty tag makes the absent state explicit so
  the stored value is overwritten with null.
- apidef/oas/security.go::extractSecurityTo is the source of truth for
  these two fields: it resets both at the top of the function and
  always emits a definite scope-map slice that mirrors the current OAS
  `security:` shape — including for cleared-scope cases where every
  alternative carries an empty array. The previous behaviour skipped
  the assignment when no alternative had a non-empty scope, which
  combined with `omitempty` left stale scopes on disk and, via
  normalizeSecurityRequirements, resurrected them on the next fill.
- apidef/oas/security.go::fillSecurity restores per-scheme scope arrays
  from SecurityRequirementScopes when present, falling back to empty
  arrays otherwise (matching the contract every other auth scheme
  follows). OAS 3.0 §4.8.30.1 guard: scope arrays are only emitted for
  oauth2 and openIdConnect schemes; every other type round-trips with
  an empty array even if upstream data carries scopes.
- normalizeSecurityRequirements is tightened to enrich-only: when the
  current SecurityRequirements list is empty it returns nil unchanged
  rather than synthesising alternatives from SecurityRequirementScopes'
  keys. The synthesis path is preserved for its original purpose
  (resizing a stale multi-requirement list to match a freshly-shrunk
  scope slice).
- apidef/oas/oauth2.go OAuth2 + OAuth2ScopeCheck types: Enabled,
  ClaimNames (default ["scope","scp"] so OAuth and OIDC tokens are
  both honored), Separator, ScopeSource. Map-probe disambiguator types
  a raw-map scheme as *OAuth2 when it carries the scopeCheck sub-block
  key, so JSON round-trips through spec save/load preserve the typed
  view. Wire-protocol constants OAuth2ErrInsufficientScope,
  OAuth2ErrInvalidToken, OAuth2AuthSchemeBearer (RFC 6750).
- Schema deltas in 4 files (oas, oas-strict, mcp, streams) carry the
  scopeCheck sub-block; scopeSource enum constraint mirrors the
  apidef validator so the dashboard rejects unknown values at save.

internal/oauth2common
- Pure-function helpers ParseUnverifiedClaims / StringClaim /
  StringFromAny used by the gateway scope-check path. No gateway or
  EE imports.

Gateway
- OAuth2Middleware reads required scopes from spec.OAS.Security. Each
  Security Requirement Object that references the oauth2 scheme
  contributes one alternative; scopes within an entry are AND-required;
  multiple entries are OR-alternatives. Declared order is preserved
  end-to-end so the operator sees their authored form on the failure
  challenge. An entry with an empty scope list ("auth required, no
  specific scope") is preserved as a vacuously-satisfied alternative
  that short-circuits the OR to "pass".
- ScopeSource selects the read path: "global" / "union" (default) read
  the root `security:` array; "operation" is a no-op in this path
  (per-operation enforcement is handled by a separate middleware).
- EnabledForSpec respects the OAS-level authentication.enabled toggle
  so disabling authentication skips the scope check too.
- Emits RFC 6750 §3 / §3.1 Bearer challenges (insufficient_scope,
  invalid_token) with stable wire shape — `Bearer SP 1#auth-param` is
  pinned by a regex assertion in tests. First alternative is cited on
  the challenge so a service-account scope isn't advertised to a
  user-token caller.
- Audit event meta carries cited alternative, full alternative list,
  granted scopes, jti/azp claim identifiers, path/method, and api_id.

Tests
- apidef/oas/security_test.go TestOAS_SecurityScopes_RoundTrip pins
  three contracts: scopes survive extract→fill for oauth2 schemes,
  non-oauth schemes get empty scope arrays per OAS spec, legacy data
  without SecurityRequirementScopes falls back to empty arrays.
- apidef/oas/security_test.go TestOAS_SecurityScopes_StalePersistence
  pins the persistence contract across six edit scenarios: scope
  cleared on a single-scheme requirement, security block removed
  entirely, multi-requirement with every scope cleared, scope replaced
  with a different value, multi-requirement shrunk to single, and
  single-requirement extended to multi. Each scenario seeds the
  APIDefinition with a stale prior value (the shape the dashboard
  would hydrate from Mongo before applying the operator's edit) and
  asserts the extract overwrites it.
- gateway/mw_oauth2_test.go covers scope extraction across claim
  formats, OR across requirements, OR-of-AND general case, multiple
  claimNames merge, default ["scope","scp"] fallback, cited-alternative
  order preservation, single-alternative citation, disabled-block and
  empty-security inert paths, RFC 6750 §3 header-shape regression
  guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@lghiur
lghiur force-pushed the TT-17173-scope-check-api-level branch from 0a3f94b to 5042b9a Compare May 19, 2026 16:41

@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

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Jira Linter Failed

Commit: af52069
Failed at: 2026-05-26 15:21:00 UTC

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

🔍 Click to view error details
failed to validate Jira issue: jira ticket TT-17173 has status 'Merge' but must be one of: Dod Check, In Dev, In Code Review, Ready For Dev

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

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

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

See analysis details on SonarQube Cloud

@lghiur
lghiur merged commit 1f5d5a7 into master May 27, 2026
180 of 195 checks passed
@lghiur
lghiur deleted the TT-17173-scope-check-api-level branch May 27, 2026 10:44
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.

4 participants