OAS JWT authentication not creating session

We’re running Tyk v5.14.0. We have an API deployed using OpenAPI. We’re trying to use JWT authentication in Tyk, but as soon as we enable it, everything starts returning { "error": "Access to this API has been disallowed" }. The Tyk logs show the following:

{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","level":"debug","message":"Started","mw":"VersionCheck","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","ts":1791995573119053033,"type":"request"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","code":200,"level":"debug","message":"Finished","mw":"VersionCheck","ns":76530,"org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","type":"request"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","level":"debug","message":"Started","mw":"RateCheckMW","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","ts":1791995573119144451,"type":"request"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","code":200,"level":"debug","message":"Finished","mw":"RateCheckMW","ns":18873,"org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","type":"request"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","level":"debug","message":"Started","mw":"AuthKey","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","ts":1791995573119174132,"type":"request"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","level":"debug","message":"Querying local cache","mw":"AuthKey","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","type":"request"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","level":"debug","message":"Querying keystore","mw":"AuthKey","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","type":"request"},
{"err":"key not found","inbound-key":"****Xjfg","level":"debug","message":"Could not get session detail, key not found","prefix":"auth-mgr","time":"2026-10-14T16:32:53Z"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","level":"debug","message":"Querying authstore","mw":"AuthKey","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","type":"request"},
{"err":"key not found","inbound-key":"****Xjfg","level":"debug","message":"Could not get session detail, key not found","prefix":"auth-mgr","time":"2026-10-14T16:32:53Z"},
{"api_id":"claims-api-v1","api_name":"Vyne Claims API v1","key":"****Xjfg","level":"info","message":"Attempted access with non-existent key.","mw":"AuthKey","org_id":"default","origin":"10.89.0.5","path":"/v1/claims","span_id":"167380e09c803bea","time":"2026-10-14T16:32:53Z","trace_id":"b8587e5773bbae473b5467889488e31e","type":"request"}

Tyk is running locally as a container in Podman managed using Aspire, with the following relevant configuration:

    // General configuration
    .WithEnvironment("TYK_GW_LOGFORMAT", "json")
    .WithEnvironment("TYK_GW_LOGLEVEL", "debug")
    .WithEnvironment("TYK_GW_LISTENPORT", dataPort.ToString(CultureInfo.InvariantCulture))
    .WithEnvironment("TYK_GW_CONTROLAPIPORT", controlPort.ToString(CultureInfo.InvariantCulture))
    .WithEnvironment("TYK_GW_SECRET", tykPassword)
    .WithEnvironment("TYK_GW_APPPATH", tykAppsPath)
    .WithEnvironment("TYK_GW_POLICIES_POLICYSOURCE", "file")
    .WithEnvironment("TYK_GW_POLICIES_POLICYPATH", tykPoliciesPath)
    // Don't use wildcard matching! If we specify a path, it had better be that path and not something vaguely similar!
    .WithEnvironment("TYK_GW_HTTPSERVEROPTIONS_ENABLESTRICTROUTES", "true")
    .WithEnvironment("TYK_GW_HTTPSERVEROPTIONS_ENABLEPATHPREFIXMATCHING", "true")
    // 10 MiB gateway-wide body limit; partners will hard-code this once published
    .WithEnvironment("TYK_GW_HTTPSERVEROPTIONS_MAXREQUESTBODYSIZE", "10485760")
    // Access logs just write to the console and are not necessarily related to OpenTelemetry
    .WithEnvironment("TYK_GW_ACCESSLOGS_ENABLED", "true")
    .WithEnvironment("TYK_GW_HEALTHCHECK_ENABLEHEALTHCHECKS", "true")
    .WithEnvironment("TYK_GW_ENABLEANALYTICS", "true")
    .WithEnvironment("TYK_GW_ANALYTICSCONFIG_ENABLEDETAILEDRECORDING", "true")
    .WithEnvironment("TYK_GW_SESSIONLIFETIMERESPECTSKEYEXPIRATION", "true")
    .WithEnvironment("TYK_GW_HASHKEYS", "false")
    .WithEnvironment("TYK_GW_HASHKEYFUNCTION", "sha256")
    .WithEnvironment("TYK_GW_ENABLEHASHEDKEYSLISTING", "true")
    .WithEnvironment("TYK_GW_USEREDISLOG", "true")

The OpenAPI document specifies:

"securitySchemes": {
  "Bearer": {
    "type": "http",
    "description": "Use HTTP Bearer authentication with a JWT obtained from the Auth service.",
    "scheme": "bearer",
    "bearerFormat": "JWT"
  }
}

Along with this relevant part of the x-tyk-api-gateway extension:

            "authentication": {
                "enabled": true,
                "stripAuthorizationData": false,
                "securitySchemes": {
                    "Bearer": {
                        "enabled": true,
                        "header": {
                            "enabled": true,
                            "name": "Authorization"
                        },
                        "signingMethod": "rsa",
                        "skipKid": false,
                        "subjectClaims": ["sub"],
                        "defaultPolicies": ["claims-api-default"],
                        "jwksURIs": [
                            {
                                "url": "https://test.stytch.com/v1/b2b/sessions/jwks/project-test-ca8e1a9c-cda5-4c8f-8f38-989d30d91b51"
                            }
                        ],
                        "allowedIssuers": ["stytch.com/project-test-ca8e1a9c-cda5-4c8f-8f38-989d30d91b51"],
                        "allowedAudiences": ["project-test-ca8e1a9c-cda5-4c8f-8f38-989d30d91b51"],
                        "customClaimValidation": {
                            "scope": {
                                "type": "contains",
                                "allowedValues": ["claims:write"]
                            }
                        }
                    }
                }
            }

I have created the claims-api-default policy as follows:

{
    "access_rights": {
        "claims-api-v1": {
            "api_id": "claims-api-v1",
            "api_name": "Vyne Claims API v1"
        }
    },
    "active": true,
    "id": "claims-api-default",
    "key_expires_in": 2592000,
    "name": "Vyne Claims API default access",
    "orgId": "default",
    "partitions": {
        "acl": true,
        "rate_limit": false,
        "quota": false,
        "complexity": false,
        "per_api": false
    }
}

I don’t understand what Tyk is doing with the JWT to create or not create a session/key. The sub claim of the JWT is m2m-client-test-93ba1026-f72e-41e2-8e2e-c9239cdbc50c, but the logs show a redacted key ending in Xjfg which appears to be the final characters of the entire JWT. Changing TYK_GW_HASHKEYS didn’t make any difference. I also don’t see any sessions or other data in the Redis cache.

Do we need to change something to get Tyk to automatically create a session for JWT authentication? The documentation explicitly says:

Once validated, Tyk creates an internal session for the request (step 5).

Hey David, welcome to the Tyk community forum!
Thank you for you question.

Based on what you have shared and looking at the mw field in your logs: every auth line says AuthKey. That appears to be Tyk’s plain API-key middleware, not the JWT middleware. So the gateway is using the default security scheme instead of what you’ve defined.

Could you confirm if your OpenAPI document has a root-level security field?

Something like this:

"security": [
  { "Bearer": [] }
]

Additionally, I noticed the claims-api-default policy has “orgId”:“default”
The key value pair is typically
"org_id":"default"

These two stand out right away. Let me know if they fix the issue.

Thanks for the quick response! We were using operation-level security requirements instead of document-level. Changing that (and fixing the incorrect org_id property name) does seem to have fixed the issue. I think we also needed "skipKid": true so it would assign the key based on the "subjectClaims" instead of the key ID.

I think there is one small issue in the documentation: bearerFormat seems to be case-sensitive, so the example of jwt won’t work; it needs to be JWT (I think this is the relevant line of code).

Brilliant, really glad that’s working now, and thank you for taking the time to write back with the specifics.

  • Operation-level security: Tyk only reads the document-level array when choosing the auth middleware, so moving it to the top was the right call. The docs don’t mention this outright, so I’ll get it added.
  • skipKid: Nice catch. Tyk uses the kid header as the identity before sub, and Stytch seems to be signing every token with the same key, so without skipKid all your clients would have shared one session.
  • bearerFormat: You’re right, it’s an exact match on JWT. I’ll update the docs and raise case-insensitive matching with the gateway team.

Thanks again for digging into the source and reporting back. Feedback like this makes the docs better for everyone.
Let me know if there is anything else I can help with.

1 Like

I do have a partially-related question. We’d like to be able to specify, on a per-operation basis, which scopes are required to access that operation (e.g. GET /foo requires foo:read whereas POST /foo requires foo:write). OpenAPI can express this, but only for OAuth/OIDC security schemes. Is it possible to combine Tyk’s external OAuth 2.0 authentication with Tyk’s JWT authentication somehow? There’s nothing in the documentation about how to configure OAuth 2.0 to require specific keys (i.e. to say “I only trust JWTs signed by this identity provider”), so it seems like you would need to use both at the same time somehow.