Trouble parsing JWT claim keys when formatted as a URL

Running into an odd situation using the “jwt” and “jwt-claims-headers” plugins: JWT claims with keys formatted as URLs are parsed incorrectly, e.g.:

"https://api.domain.tld/tenant-id": "1" is transformed to "https":"//api.domain.tld/tenant-id: 1"

Not sure which plugin is responsible for the transformation. Auth0 uses URLs to create namespaces for claim keys to avoid collisions.

Any ideas on where to start looking for a fix?

Can you include your plugin configuration (without revealing private data)?
Do you use any other plugins?
What is the complete initial request (without including any private data)?

plugins:

{
  "total": 2,
  "data": [
    {
      "created_at": 1513889919000,
      "config": {
        "secret_is_base64": false,
        "key_claim_name": "iss",
        "anonymous": "",
        "run_on_preflight": true,
        "uri_param_names": [
          "jwt"
        ]
      },
      "id": "fb90e0f4-a1eb-4d7c-98bc-bd94e76e9bf8",
      "enabled": true,
      "api_id": "c55f4841-48f4-4124-adc1-e1c6a6221cef",
      "name": "jwt"
    },
    {
      "created_at": 1513890263000,
      "config": {
        "claims_to_include": [
          ".*"
        ],
        "continue_on_error": true,
        "uri_param_names": [
          "jwt"
        ]
      },
      "id": "70e339bb-f9c1-483b-b46a-d915ad7cd3cb",
      "enabled": true,
      "api_id": "c55f4841-48f4-4124-adc1-e1c6a6221cef",
      "name": "jwt-claims-headers"
    }
  ]
}

sample request/response:

$ curl -v localhost:8000/example-jwt/info -H "Authorization: Bearer $token" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /example-jwt/info HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5rRkJOakZCUTBVd1FqTkdPVFl5UWpaR1FqVXdNamRDTkVJMU0wVkJOMFV3UWtJMVF6WTJOdyJ9.eyJodHRwczovL2FwaS5vY2VhbnguY29tL09DWC1URU5BTlQiOiIxIiwiaHR0cHM6Ly9hcGkub2NlYW54LmNvbS9wZXJtaXNzaW9ucyI6W10sImh0dHBzOi8vYXBpLm9jZWFueC5jb20vZ3JvdXBzIjpbIkZpbmFuY2UiXSwiaHR0cHM6Ly9hcGkub2NlYW54LmNvbS9yb2xlcyI6W10sImlzcyI6Imh0dHBzOi8vdGhlYWQtdGVzdC5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NWEzODFmNjhiYzkwMWI3OWI5YTg0Yzk2IiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODAwMC9leGFtcGxlLWp3dCIsImh0dHBzOi8vdGhlYWQtdGVzdC5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNTEzOTY4ODg2LCJleHAiOjE1MTM5NzYwODYsImF6cCI6IlE2WVQxZ3N5d3dycEJCYXJWTTBZN01zWVZZWjlPZnYwIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSJ9.k2Q9h8lj_nWD6OBWc_HdxfF7_DbzL9Smy0N8vy4PA4ylgSqVqX1ZsCF2jF4XOKyUHM8rqO49XbR8_YQRPCQ1ltViQ-HWqYCB8bQfN_3_DVW5dnh2_JbQbegiXxwEJhYAPnkDLudv1zNNlFtCcnGiVInZyAkTwyS8ps2rQc6e-qZQjwR6T7z5DnhdmRm0YNbt_VndsdmZ32EdLIfdLa5kPzFwozrZ5Z_8faokHmYdXiJPeLv4H22ONbm9ndk0BsXy-7hYy7aQq0Mg1OmR_EzMHkw_svwwSrD74ix1PwvbCWJ_wYHx4uwpbmBon5eiKGONbwsew_qYOSnuSbGS20j_LQ
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: thin
< X-Kong-Upstream-Latency: 11
< X-Kong-Proxy-Latency: 0
< Via: kong/0.11.2
<
{ [1851 bytes data]
100  1844    0  1844    0     0  91490      0 --:--:-- --:--:-- --:--:-- 97052
* Connection #0 to host localhost left intact
{
  "HTTP_ACCEPT": "*/*",
  "HTTP_AUTHORIZATION": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5rRkJOakZCUTBVd1FqTkdPVFl5UWpaR1FqVXdNamRDTkVJMU0wVkJOMFV3UWtJMVF6WTJOdyJ9.eyJodHRwczovL2FwaS5vY2VhbnguY29tL09DWC1URU5BTlQiOiIxIiwiaHR0cHM6Ly9hcGkub2NlYW54LmNvbS9wZXJtaXNzaW9ucyI6W10sImh0dHBzOi8vYXBpLm9jZWFueC5jb20vZ3JvdXBzIjpbIkZpbmFuY2UiXSwiaHR0cHM6Ly9hcGkub2NlYW54LmNvbS9yb2xlcyI6W10sImlzcyI6Imh0dHBzOi8vdGhlYWQtdGVzdC5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NWEzODFmNjhiYzkwMWI3OWI5YTg0Yzk2IiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODAwMC9leGFtcGxlLWp3dCIsImh0dHBzOi8vdGhlYWQtdGVzdC5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNTEzOTY4ODg2LCJleHAiOjE1MTM5NzYwODYsImF6cCI6IlE2WVQxZ3N5d3dycEJCYXJWTTBZN01zWVZZWjlPZnYwIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSJ9.k2Q9h8lj_nWD6OBWc_HdxfF7_DbzL9Smy0N8vy4PA4ylgSqVqX1ZsCF2jF4XOKyUHM8rqO49XbR8_YQRPCQ1ltViQ-HWqYCB8bQfN_3_DVW5dnh2_JbQbegiXxwEJhYAPnkDLudv1zNNlFtCcnGiVInZyAkTwyS8ps2rQc6e-qZQjwR6T7z5DnhdmRm0YNbt_VndsdmZ32EdLIfdLa5kPzFwozrZ5Z_8faokHmYdXiJPeLv4H22ONbm9ndk0BsXy-7hYy7aQq0Mg1OmR_EzMHkw_svwwSrD74ix1PwvbCWJ_wYHx4uwpbmBon5eiKGONbwsew_qYOSnuSbGS20j_LQ",
  "HTTP_CONNECTION": "keep-alive",
  "HTTP_HOST": "example_api:3000",
  "HTTP_USER_AGENT": "curl/7.43.0",
  "HTTP_VERSION": "HTTP/1.1",
  "HTTP_X_AUD": "https://thead-test.auth0.com/userinfo",
  "HTTP_X_AZP": "Q6YT1gsywwrpBBarVM0Y7MsYVYZ9Ofv0",
  "HTTP_X_CONSUMER_ID": "3d7487b3-f03e-49b5-8e63-2aa0b635353e",
  "HTTP_X_CONSUMER_USERNAME": "thead",
  "HTTP_X_EXP": "1513976086",
  "HTTP_X_FORWARDED_FOR": "172.18.0.1",
  "HTTP_X_FORWARDED_HOST": "localhost",
  "HTTP_X_FORWARDED_PORT": "8000",
  "HTTP_X_FORWARDED_PROTO": "http",
  "HTTP_X_HTTPS": "//api.oceanx.com/OCX-TENANT: 1",
  "HTTP_X_IAT": "1513968886",
  "HTTP_X_ISS": "https://thead-test.auth0.com/",
  "HTTP_X_REAL_IP": "172.18.0.1",
  "HTTP_X_SCOPE": "openid profile",
  "HTTP_X_SUB": "auth0|5a381f68bc901b79b9a84c96",
  "QUERY_STRING": "",
  "REQUEST_METHOD": "GET",
  "REQUEST_PATH": "/info"

The “jwt-claims-headers” plugin is from https://github.com/wshirey/kong-plugin-jwt-claims-headers

Did some more sleuthing on this issue. I managed to insert some print statements at the point where I think things are going wrong. The ‘jwt-claims-headers’ plugin is looping over a value named ‘jwt.claims’, which seems to be a lua ‘table’ (forgive my ignorance). Dumping the keys and values with ‘print(inspect(…))’ shows that the URL keys are treated differently, for example:

  (snip)
  azp = "Q6YT1gsywwrpBBarVM0Y7MsYVYZ9Ofv0",
  exp = 1514097026,
  ["https://api.somedomain.com/TENANT"] = "1",
  (snip)

Is that an array notation? or something else?

In any case, it seems the issue is related to the ‘jwt-claims-headers’ plugin.