Bug Report: JWT scopes.claims field dropped from OAS APIs — Operator CRD missing 5.10+ field
Date: 2026-04-08
Severity: High — JWT scope-to-policy mapping silently breaks
Environment
| Component |
Version |
| Tyk Dashboard |
v5.11.1 (broken) / v5.8.9 (working) |
| Tyk Gateway |
v5.11.1 (broken) / v5.8.9 (working) |
| Tyk Pump |
v1.13.3 (broken) / v1.12.0 (working) |
| Tyk Operator |
v1.2.0 (unchanged in both scenarios) |
| Operator CRD |
v3.0.0 (latest, also checked v1.3.1) |
| Helm |
v4.1.3 |
| Kubernetes |
v1.35.2 (k3d) |
Summary
When deploying OAS APIs as code using the Tyk Operator with TykOasApiDefinition CRDs that include jwtAuth.scopes, the claims field in x-tyk-api-gateway.server.authentication.securitySchemes.jwtAuth.scopes is silently dropped from the resulting API definition stored in the Dashboard.
Critically, scopes.claims is the official replacement for scopes.claimName from Tyk 5.10+ (per JWT Authorization docs). The Tyk Operator CRD does not support this field even in the latest version (v1.3.1 / CRD v3.0.0), making it impossible to configure JWT scope authorization using the documented 5.10+ approach via the operator.
This was observed after upgrading the Dashboard and Gateway images from v5.8.9 to v5.11.1. Reverting the Dashboard back to v5.8.9 (without changing anything else) resolved the issue, because in v5.8.9 the field was preserved as opaque JSON.
Observation
What we deployed
OAS API definitions via Helm charts that render:
-
A ConfigMap containing the full OAS spec with claims in scope configuration:
# In the OAS spec (ConfigMap)
x-tyk-api-gateway:
server:
authentication:
securitySchemes:
jwtAuth:
scopes:
claimName: "roles"
claims:
- "roles"
-
A TykOasApiDefinition CRD with jwtAuth.scopes for operator-managed policy references:
apiVersion: tyk.tyk.io/v1alpha1
kind: TykOasApiDefinition
spec:
tykOAS:
configmapRef:
name: my-api-oas
keyName: oas.yaml
jwtAuth:
scopes:
claimName: "roles"
scopeToPolicyMapping:
- scope: "some-scope-develop"
policyRef:
name: my-policy
namespace: tyk-develop
The Tyk Operator reconciles these two resources: it resolves policyRef names to policy IDs and replaces the entire scopes object in the OAS spec with one constructed from the CRD fields. Since the CRD has no claims field, the original claims: ["roles"] from the ConfigMap is discarded.
What happened
- With Dashboard v5.8.9: The
claims: ["roles"] field is present in the API definition on the Dashboard. JWT scope-to-policy mapping works correctly.
- With Dashboard v5.11.1: The
claims field is missing from the API definition on the Dashboard. Only claimName and scopeToPolicyMapping (with resolved policy IDs) remain.
How we verified
# Get the API from the Dashboard API
TYK_AUTH=$(kubectl get secret tyk-operator-conf -n tyk -o jsonpath='{.data.TYK_AUTH}' | base64 -d)
API_ID=$(kubectl get tykoasapidefinition -n tyk-develop <api-name> -o jsonpath='{.status.id}')
curl -s "http://localhost:3000/api/apis/oas/$API_ID" \
-H "Authorization: $TYK_AUTH" | \
jq '.["x-tyk-api-gateway"].server.authentication.securitySchemes.jwtAuth.scopes'
Dashboard v5.8.9 result (correct):
{
"claimName": "roles",
"claims": ["roles"],
"scopeToPolicyMapping": [{ "policyId": "...", "scope": "some-scope-develop" }]
}
Dashboard v5.11.1 result (broken):
{
"claimName": "roles",
"scopeToPolicyMapping": [{ "policyId": "...", "scope": "some-scope-develop" }]
}
Control test
APIs without jwtAuth.scopes in the CRD (i.e. the operator does not touch the scopes object) retain claims: ["roles"] on both Dashboard versions. This confirms the issue only occurs when the operator actively manages the scopes section.
Root Cause Analysis
Three factors combine to cause this:
1. Operator CRD missing claims field introduced in Tyk 5.10+
The Tyk documentation (JWT Authorization) states:
Prior to Tyk 5.10, the authorization scopes claim was retrieved from scopes.claimName. From 5.10+, scopes.claims (array) replaces scopes.claimName (single string).
The configuration summary table shows:
| Config |
Pre-5.10 |
5.10+ |
| Scope claim(s) |
scopes.claimName |
scopes.claims |
The docs also state: "If both old and new fields are specified, the new fields take precedence."
However, the Tyk Operator CRD (TykOasApiDefinition) — even in the latest version (v1.3.1 / CRD v3.0.0, from tyk-charts main branch) — only has:
jwtAuth:
scopes:
claimName: string # ✅ Present (legacy field, pre-5.10)
scopeToPolicyMapping: [] # ✅ Present
# claims: []string # ❌ MISSING (new field, 5.10+)
This means the operator physically cannot express the 5.10+ recommended claims field. Users who need multiple claims (e.g., claims: ["roles", "permissions"]) have no way to configure this via the CRD.
2. Tyk Operator replaces (not merges) the scopes object
When jwtAuth.scopes is present in the CRD, the operator replaces the entire scopes object in the OAS spec with a version constructed from the CRD fields. The original claims from the ConfigMap is discarded.
3. Dashboard v5.11.x added a typed Claims field to the Scopes Go struct
In the Tyk Gateway/Dashboard codebase (apidef/oas/authentication.go):
v5.8.9 — Scopes struct:
type Scopes struct {
ClaimName string `json:"claimName,omitempty"`
ScopeToPolicyMapping []ScopeToPolicy `json:"scopeToPolicyMapping,omitempty"`
}
v5.11.1 — Scopes struct:
type Scopes struct {
ClaimName string `json:"claimName,omitempty"`
Claims []string `json:"claims,omitempty"`
ScopeToPolicyMapping []ScopeToPolicy `json:"scopeToPolicyMapping,omitempty"`
}
In v5.8.9, claims was an unknown JSON field not mapped to any Go struct field. It was preserved in the stored JSON because the Dashboard's internal representation did not strip unknown fields.
In v5.11.1, claims is a typed struct field with omitempty. When the operator sends the API spec without claims, the Dashboard deserializes it as nil and re-serializes it without the field (due to omitempty).
The Fill method added in v5.11.x auto-generates claims from claimName, but this codepath is only reached during classic → OAS conversion, not for native OAS API creation/updates (which is what the operator does).
Impact
- JWT scope-to-policy mapping may fail if the gateway requires the
claims field to determine which JWT claim(s) to interrogate for scope values
- Affects all OAS APIs deployed via Tyk Operator that use
jwtAuth.scopes in the CRD
- Silent failure — no error is logged by the operator or dashboard
Workarounds
- Stay on Dashboard v5.8.9 — the version where
claims is preserved as an opaque JSON field
- Remove
jwtAuth.scopes from the CRD — pre-compute the deterministic operator policy IDs (base64(namespace/name) without padding) and put the full scopeToPolicyMapping (including claims) directly in the OAS spec ConfigMap, so the operator does not touch the scopes object
- Use a post-deploy hook that patches
claims back into each API via the Dashboard API after operator reconciliation
Suggested Fix
The primary fix should address the CRD schema gap:
- Tyk Operator CRD (primary): Add a
claims field (type []string) to the jwtAuth.scopes CRD schema and pass it through when constructing the scopes object for the OAS spec. This brings the CRD in line with the 5.10+ API changes documented at JWT Authorization.
- Tyk Operator reconciliation: Merge the CRD's
scopes fields into the existing OAS spec scopes object instead of replacing it entirely. This would prevent losing any fields that exist in the ConfigMap but not in the CRD, and would be more resilient to future additions to the OAS spec.
- Tyk Dashboard (secondary): When receiving an OAS spec update, preserve existing fields in
scopes that are not explicitly set in the incoming payload, rather than replacing the entire object.
Documentation Reference
Bug Report: JWT
scopes.claimsfield dropped from OAS APIs — Operator CRD missing 5.10+ fieldDate: 2026-04-08
Severity: High — JWT scope-to-policy mapping silently breaks
Environment
Summary
When deploying OAS APIs as code using the Tyk Operator with
TykOasApiDefinitionCRDs that includejwtAuth.scopes, theclaimsfield inx-tyk-api-gateway.server.authentication.securitySchemes.jwtAuth.scopesis silently dropped from the resulting API definition stored in the Dashboard.Critically,
scopes.claimsis the official replacement forscopes.claimNamefrom Tyk 5.10+ (per JWT Authorization docs). The Tyk Operator CRD does not support this field even in the latest version (v1.3.1 / CRD v3.0.0), making it impossible to configure JWT scope authorization using the documented 5.10+ approach via the operator.This was observed after upgrading the Dashboard and Gateway images from v5.8.9 to v5.11.1. Reverting the Dashboard back to v5.8.9 (without changing anything else) resolved the issue, because in v5.8.9 the field was preserved as opaque JSON.
Observation
What we deployed
OAS API definitions via Helm charts that render:
A ConfigMap containing the full OAS spec with
claimsin scope configuration:A TykOasApiDefinition CRD with
jwtAuth.scopesfor operator-managed policy references:The Tyk Operator reconciles these two resources: it resolves
policyRefnames to policy IDs and replaces the entirescopesobject in the OAS spec with one constructed from the CRD fields. Since the CRD has noclaimsfield, the originalclaims: ["roles"]from the ConfigMap is discarded.What happened
claims: ["roles"]field is present in the API definition on the Dashboard. JWT scope-to-policy mapping works correctly.claimsfield is missing from the API definition on the Dashboard. OnlyclaimNameandscopeToPolicyMapping(with resolved policy IDs) remain.How we verified
Dashboard v5.8.9 result (correct):
{ "claimName": "roles", "claims": ["roles"], "scopeToPolicyMapping": [{ "policyId": "...", "scope": "some-scope-develop" }] }Dashboard v5.11.1 result (broken):
{ "claimName": "roles", "scopeToPolicyMapping": [{ "policyId": "...", "scope": "some-scope-develop" }] }Control test
APIs without
jwtAuth.scopesin the CRD (i.e. the operator does not touch thescopesobject) retainclaims: ["roles"]on both Dashboard versions. This confirms the issue only occurs when the operator actively manages thescopessection.Root Cause Analysis
Three factors combine to cause this:
1. Operator CRD missing
claimsfield introduced in Tyk 5.10+The Tyk documentation (JWT Authorization) states:
The configuration summary table shows:
scopes.claimNamescopes.claimsThe docs also state: "If both old and new fields are specified, the new fields take precedence."
However, the Tyk Operator CRD (
TykOasApiDefinition) — even in the latest version (v1.3.1 / CRD v3.0.0, from tyk-charts main branch) — only has:This means the operator physically cannot express the 5.10+ recommended
claimsfield. Users who need multiple claims (e.g.,claims: ["roles", "permissions"]) have no way to configure this via the CRD.2. Tyk Operator replaces (not merges) the
scopesobjectWhen
jwtAuth.scopesis present in the CRD, the operator replaces the entirescopesobject in the OAS spec with a version constructed from the CRD fields. The originalclaimsfrom the ConfigMap is discarded.3. Dashboard v5.11.x added a typed
Claimsfield to theScopesGo structIn the Tyk Gateway/Dashboard codebase (
apidef/oas/authentication.go):v5.8.9 —
Scopesstruct:v5.11.1 —
Scopesstruct:In v5.8.9,
claimswas an unknown JSON field not mapped to any Go struct field. It was preserved in the stored JSON because the Dashboard's internal representation did not strip unknown fields.In v5.11.1,
claimsis a typed struct field withomitempty. When the operator sends the API spec withoutclaims, the Dashboard deserializes it asniland re-serializes it without the field (due toomitempty).The
Fillmethod added in v5.11.x auto-generatesclaimsfromclaimName, but this codepath is only reached during classic → OAS conversion, not for native OAS API creation/updates (which is what the operator does).Impact
claimsfield to determine which JWT claim(s) to interrogate for scope valuesjwtAuth.scopesin the CRDWorkarounds
claimsis preserved as an opaque JSON fieldjwtAuth.scopesfrom the CRD — pre-compute the deterministic operator policy IDs (base64(namespace/name)without padding) and put the fullscopeToPolicyMapping(includingclaims) directly in the OAS spec ConfigMap, so the operator does not touch thescopesobjectclaimsback into each API via the Dashboard API after operator reconciliationSuggested Fix
The primary fix should address the CRD schema gap:
claimsfield (type[]string) to thejwtAuth.scopesCRD schema and pass it through when constructing thescopesobject for the OAS spec. This brings the CRD in line with the 5.10+ API changes documented at JWT Authorization.scopesfields into the existing OAS specscopesobject instead of replacing it entirely. This would prevent losing any fields that exist in the ConfigMap but not in the CRD, and would be more resilient to future additions to the OAS spec.scopesthat are not explicitly set in the incoming payload, rather than replacing the entire object.Documentation Reference
scopes.claimsas the 5.10+ replacement forscopes.claimNameclaimsfield injwtAuth.scopes