JWT JWKs error "Key must be PEM encoded PKCS1 or PKCS8 private key"

I’m playing around with Tyk doing a POC to see how easily we could integrate it into our platform. We currently use Auth0 for our identity management and generate access tokens which get passed to our backend services.

I was trying to configure a dummy API in Tyk that could verify JWT tokens generated by Auth0 using the JWKs endpoint, so if the certs are rolled Tyk can still verify them. I added a very simple API that integrated with the default httpbin service, configured the JWT auth to be RSA and added my JWKs url into the public key field which ends up as the jwt_source config option.

When I call the API with my Authorization header with the value Bearer xxx.yyy.zzz I get the following error from Tyk "Key not authorized:Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key"

The JWT token I’m sending is valid and the header has a kid which matches the kid in the JWKs response.

To be honest, I’ve got no idea what Tyk could be trying to do. Is it thinking the JWKs url is actually a text PEM?

The raw definition of the API is below, where the real value of my JWKs url is https://pupil-test.eu.auth0.com/.well-known/jwks.json.

Any help would be greatly appreciated.

James

{
  "api_model": {},
  "api_definition": {
    "api_id": "26f2a40cb46e4fb75e425120ceefea50",
    "jwt_issued_at_validation_skew": 0,
    "upstream_certificates": {},
    "use_keyless": false,
    "enable_coprocess_auth": false,
    "custom_middleware": {
      "pre": [],
      "post": [],
      "post_key_auth": [],
      "auth_check": {
        "name": "",
        "path": "",
        "require_session": false
      },
      "response": [],
      "driver": "",
      "id_extractor": {
        "extract_from": "",
        "extract_with": "",
        "extractor_config": {}
      }
    },
    "disable_quota": false,
    "custom_middleware_bundle": "",
    "cache_options": {
      "cache_timeout": 60,
      "enable_cache": true,
      "cache_all_safe_requests": false,
      "cache_response_codes": [],
      "enable_upstream_cache_control": false,
      "cache_control_ttl_header": ""
    },
    "enable_ip_blacklisting": false,
    "tag_headers": [],
    "pinned_public_keys": {},
    "expire_analytics_after": 0,
    "domain": "",
    "openid_options": {
      "providers": [],
      "segregate_by_client": false
    },
    "jwt_policy_field_name": "",
    "active": true,
    "jwt_expires_at_validation_skew": 0,
    "config_data": {},
    "notifications": {
      "shared_secret": "",
      "oauth_on_keychange_url": ""
    },
    "auth": {
      "auth_header_name": "Authorization",
      "use_certificate": false
    },
    "check_host_against_uptime_tests": false,
    "auth_provider": {
      "name": "",
      "storage_engine": "",
      "meta": {}
    },
    "blacklisted_ips": [],
    "hmac_allowed_clock_skew": -1,
    "dont_set_quota_on_create": false,
    "uptime_tests": {
      "check_list": [],
      "config": {
        "expire_utime_after": 0,
        "service_discovery": {
          "use_discovery_service": false,
          "query_endpoint": "",
          "use_nested_query": false,
          "parent_data_path": "",
          "data_path": "",
          "cache_timeout": 60
        },
        "recheck_wait": 0
      }
    },
    "enable_jwt": true,
    "do_not_track": false,
    "name": "Test",
    "slug": "test",
    "oauth_meta": {
      "allowed_access_types": [],
      "allowed_authorize_types": [],
      "auth_login_redirect": ""
    },
    "CORS": {
      "enable": false,
      "max_age": 24,
      "allow_credentials": false,
      "exposed_headers": [],
      "allowed_headers": [],
      "options_passthrough": false,
      "debug": false,
      "allowed_origins": [],
      "allowed_methods": []
    },
    "event_handlers": {
      "events": {}
    },
    "proxy": {
      "target_url": "http://httpbin.org/",
      "endpoint_returns_list": false,
      "cache_timeout": 0,
      "parent_data_path": "",
      "service_discovery": {
        "endpoint_returns_list": false,
        "cache_timeout": 0,
        "parent_data_path": "",
        "query_endpoint": "",
        "use_discovery_service": false,
        "target_path": "",
        "use_target_list": false,
        "use_nested_query": false,
        "data_path": "",
        "port_data_path": ""
      },
      "check_host_against_uptime_tests": false,
      "transport": {
        "ssl_ciphers": [],
        "ssl_min_version": 0,
        "proxy_url": ""
      },
      "target_list": [],
      "query_endpoint": "",
      "use_discovery_service": false,
      "_sd_show_port_path": false,
      "preserve_host_header": false,
      "use_target_list": false,
      "strip_listen_path": true,
      "use_nested_query": false,
      "data_path": "",
      "port_data_path": "",
      "enable_load_balancing": false,
      "listen_path": "/26f2a40cb46e4fb75e425120ceefea50/",
      "disable_strip_slash": false
    },
    "client_certificates": [],
    "use_basic_auth": false,
    "version_data": {
      "not_versioned": true,
      "default_version": "",
      "versions": {
        "Default": {
          "name": "Default",
          "expires": "",
          "paths": {
            "ignored": [],
            "white_list": [],
            "black_list": []
          },
          "use_extended_paths": true,
          "extended_paths": {
            "ignored": [],
            "white_list": [],
            "black_list": [],
            "transform": [],
            "transform_response": [],
            "transform_jq": [],
            "transform_jq_response": [],
            "transform_headers": [],
            "transform_response_headers": [],
            "hard_timeouts": [],
            "circuit_breakers": [],
            "url_rewrites": [],
            "virtual": [],
            "size_limits": [],
            "method_transforms": [],
            "track_endpoints": [],
            "do_not_track_endpoints": [],
            "validate_json": []
          },
          "global_headers": {},
          "global_headers_remove": [],
          "global_size_limit": 0,
          "override_target": ""
        }
      }
    },
    "use_standard_auth": false,
    "session_lifetime": 0,
    "hmac_allowed_algorithms": [],
    "disable_rate_limit": false,
    "definition": {
      "location": "header",
      "key": "x-api-version",
      "strip_path": false
    },
    "use_oauth2": false,
    "jwt_source": "aHR0cHM6Ly9wdXBpbC10ZXN0LmV1LmF1dGgwLmNvbS8ud2VsbC1rbm93bi9qd2tzLmpzb24=",
    "jwt_signing_method": "rsa",
    "jwt_not_before_validation_skew": 0,
    "jwt_identity_base_field": "sub",
    "allowed_ips": [],
    "org_id": "5c49e86873ca350001ca5bf0",
    "enable_ip_whitelisting": false,
    "global_rate_limit": {
      "rate": 0,
      "per": 0
    },
    "enable_context_vars": false,
    "tags": [],
    "basic_auth": {
      "disable_caching": false,
      "cache_ttl": 0
    },
    "session_provider": {
      "name": "",
      "storage_engine": "",
      "meta": {}
    },
    "strip_auth_data": false,
    "id": "5c50844c7db45f000112c00d",
    "certificates": [],
    "enable_signature_checking": false,
    "use_openid": false,
    "jwt_skip_kid": false,
    "enable_batch_request_support": false,
    "response_processors": [],
    "use_mutual_tls_auth": false
  },
  "hook_references": [],
  "is_site": false,
  "sort_by": 0
}

For you, or for anyone else who has also tried their hand at getting this to work - you are not crazy. It does not work - at least not with a standards-compliant jwks.json file

I have also been experimenting with the JWT Authorization configuration, trying to get an API to accept the Access Token issued by an OIDC Provider (in my case is it also happens to be Auth0), and finding the same error you do.

From the logs, I can also see “Failed to decode JWT to RSA type”. Using this breadcrumb, I started to hunt and peck through the codebase and found:

  1. If you put a URL in the field for “Public Key” in the Dashboard (equivalent, I believe, to putting the base64-encoded version of the URL into the “jwt_source” field in the API definition), Tyk will fetch the jwks.json file specified
  2. It unpacks the json of the jwks
  3. It grabs the first value in x5c and uses it as the cert
  4. It then tries to decode the value as a PEM file

The issue with 4 is that the specification of x5c is to be base64-encoded DER and not base64-encoded PEM, like this code wants.

This is known to be non-standard with the recommendation being to use OpenID Connect as an auth mechanism.

The issue with that is that the OpenID Connect mechanism expects users (or OAuth2 clients, whatever the case is) to use ID Tokens. This, however, is not recommended because the Access Token is supposed to be used for accessing APIs on the Resource Server. That is, standards-compliant clients will be expecting to send the Access Token, and those will get rejected (unless, of course, you could tweak the aud parameter injected into the Access Token; this is not possible with Auth0).

If you have the time and the inclination to do so, one way you could get around this issue would be to have a custom server that could translate the x5c parameter from DER to PEM on the fly (i.e. GET https://tykx5ctranslator.com/?jwksSource=https%3A%2F%2Fmytenant.auth0.com%2F.well-known%2Fjwks.json )
I don’t have either, so I’m probably going to go skipping the use of a JWKs URL, and instead specifying the PEM file for each API definition.

Either that or replacing Tyk in our stack.

3 Likes

Great analysis! Please do log an issue if you’d like to open a discussion about the using DER instead of PEM?