Skip to content

Bug Report: JWT claims field dropped from OAS APIs when using Tyk Operator with Dashboard > v5.8.x #472

Description

@m4s-b3n

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:

  1. 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"
  2. 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.9Scopes struct:

type Scopes struct {
    ClaimName            string          `json:"claimName,omitempty"`
    ScopeToPolicyMapping []ScopeToPolicy `json:"scopeToPolicyMapping,omitempty"`
}

v5.11.1Scopes 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

  1. Stay on Dashboard v5.8.9 — the version where claims is preserved as an opaque JSON field
  2. 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
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternal

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions