Multiple auth schemes for single API definition

After further analysis, it appear using JWT everywhere could get pretty close to meeting our requirements. It would be really useful if when specifying a kid, it pointed to a key with secret that then creates the virtual policy based on JWT sub from a JWT claim specified policy. This is what I trying to accomplish when you enlightened me that I was conflating the key and policy concepts and would get a single key rate limit for all clients/users, not the JWT sub based virtual policy.

It really isn’t ideal to have to create an API definition for each customer so they can use unique secrets. We currently have many thousands of customers each with 100s to many thousands of users. It’s just not practical to create/manage 50K+ API definitions just for unique customer secrets. And that’s just for the front-end apps, if we assume 10+ back-end services, it will be 100K+ API definitions only to support unique secrets. How would Tyk perform if there were that many API definitions?

If it were possible to map to keys, we would have one API definition for each API instead of one per customer. There would be one key per customer per API, and a few policies mapped by JWT claim based on the various user roles. This would be much nicer and far more flexible and manageable. I’m assuming it would basically require modifying the key to use the virtual policy like already exists without a key instead of a static one. The JWT validation secret needs to be specified in the key not API - already exists. It would also be nice if we could inject the kid in the JWT data instead of the header since we can accomplish that with Auth0.

I’ve crafted a temporary workaround that requires customer+1 API definitions per service since Tyk doesn’t currently have the above support.

Paste the following here to see sequence diagram

title Browser Direct Request wo Proxy/Delegation

User->Browser: Nav to cust DNS
Browser->WebServer: Load app
User->Browser: Login
Browser->AppService: get cust info
AppService->DB: get cust info
note right of AppService: Auth0 clientID, domain
AppService->Browser: return cust info
Browser->Auth0: redirect
User->Auth0: authenticate
Auth0->Browser: redirect back to Skillsoft app
User->Browser: click submit for request
Browser->SecurityService: getAuthJWT(JWT, app, method, dataSpec)
note right of SecurityService: ideally caching
SecurityService->SecurityService: validate JWT
SecurityService->FooApp:isAuthorized(clientId, method, dataSpec)
SecurityService->SecurityService: create/sign JWT w/FooApp secret
SecurityService->Browser: return FooApp secret signed JWT
Browser->FooApp: apiRequest(FooApp secret signed JWT)

There is a feature that may be help you with this, it’s a bit of a bastardisation of the JWK format, but it allows you to specify multiple secrets for a single API Definition, so that you can target them using a kid. In this case the kid refers to a Secret ID (the key in this sense being the RSA key).

This isn’t currently documented because it doesn’t fully follow the JWK spec. However it works just fine for use cases where you have one API and multiple IDPs with different RSA Public keys and you want to validate them separately but manage them centrally.

With the JWK method, Tyk will:

  1. Identify the KID from the inbound JWT
  2. Pull the JWK file from the JWK service provider
  3. Check the KID against those listed in the JWK
  4. If the KID exists in the JWK key-list, it will try to verify the signature of the JWT with the first key in the JWK keys list
  5. If the JWK is valid, extract the sub and policy_id claims to generate a virtual token based on the policy, tied to the bearer

This flow describes what you are trying to achieve: Multiple Secrets → One API → Multiple Policies → Virtual token

To get this to work, you create a document on a server that only Tyk Gateway can see and should use HTTPS, this will be a public-key source, let’s say we call the file idp_jwks.json and this file is hosted on https://my-internal-server.com/idp_jwks.json:

idp_jwks.json:

{
	"keys": [{
		"alg": "RS256",
		"kty": "RSA",
		"use": "sig",
		"x5c": ["LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ3FHS3VrTzFEZTd6aFpqNitIMHF0alRrVnh3VENwdktlNGVDWjANCkZQcXJpMGNiMkpaZlhKL0RnWVNGNnZVcHdtSkc4d1ZRWktqZUdjakRPTDVVbHN1dXNGbmNDeldCUTdSS05VU2VzbVFSTVNHa1ZiMS8NCjNqK3NrWjZVdFcrNXUwOWxITnNqNnRRNTFzMVNQckNCa2VkYk5mMFRwMEdiTUpEeVI0ZTlUMDRaWndJREFRQUINCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ=="],
                "kid": "12345",
	        },
{
		"alg": "RS256",
		"kty": "RSA",
		"use": "sig",
		"x5c": ["LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ3FHS3VrTzFEZTd6aFpqNitIMHF0alRrVnh3VENwdktlNGVDWjANCkZQcXJpMGNiMkpaZlhKL0RnWVNGNnZVcHdtSkc4d1ZRWktqZUdjakRPTDVVbHN1dXNGbmNDeldCUTdSS05VU2VzbVFSTVNHa1ZiMS8NCjNqK3NrWjZVdFcrNXUwOWxITnNqNnRRNTFzMVNQckNCa2VkYk5mMFRwMEdiTUpEeVI0ZTlUMDRaWndJREFRQUINCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ=="],
                "kid": "9999999",
	        }]
}

As you can see this document contains a list of RSA Public keys (the key is in the x5c property - this is not on-spec), these keys are a base64-encoded version of the PEM-encoded version of the RSA public key (if you decode it you’ll see a traditional public key ascii file format - this is where we also differ from the JWK spec). All these keys are delimited by their key ID.

Now when you set up your API that needs to validate multiple JWT sources (i.e. you may not want Auth0 providing the credentials for your internal app-to-app comms) then you have one key that is Auth0’s public key and another that is for your internal app-to-app stuff (in fact, you can have as many as you like).

In your Tyk API Definition, in the JWT secret field, instead of a public key, you just put our custom JWK URL:

https://my-internal-server.com/idp_jwks.json

Now when a JWT comes in, so long as the kid matches one in the JWK file (this file is cached periodically to speed things up) it will validate and apply the policy specified in the policy ID field claim of the JWT so you can rate limit on a per-client, per-source basis.

With the above, this is possible:

  1. An API has a JWK instead of a single secret
  2. Each customer’s secret is set as a KID in the JWK, so you can extend this as you will, it will refresh over time (a hot reload should also force this)
  3. You create a few policies (e.g. AppToApp, CustomerTier1, CustomerTier2, CustomerTier3 and Enterprise)
  4. Depending on the IDP or source of the JWT, you set the appropriate policy_id as a claim in the JWT so that uers are rate limited differently depending on where they come from.

This functionality has been part of Tyk for a while, but isn’t documented because our JWK documents are non-spec and we do not follow the spec in full, e.g. we don’t check the key-type field, we assume the key type is the same as that of the API Definition.

Full compliance is in our OIDC middleware though - but that is fully compliant with the whole set of specs associated with OpenID Connect, and is totally separate from our custom JWT implementation (which offers a bit of flex). OIDC is available in nightlies and will be available as a full release in a few weeks.

I hope that helps!

M.

1 Like

This is pretty slick! It certainly appears this will meet our needs functionally, however I’m concerned if it will perform adequately.

A few questions:

  • Above you say first key in JWK keys list, I assume you mean first matching kid/key in JWK keys list?
  • This is RSA (slow) vs HMAC (fast), since every API request will validate the JWT at Tyk, will it perform satisfactorily with RSA?
  • There will be 6000+ keys specified in the idp_jwks.json file, how will that perform - is it a map lookup or scan?
1 Like

Yes, that too, if the keys array contains two or more entries with the same KID then the first will get matched.

Also, in the entry for a kid, you’ll see that the x5c parameter is an array, according to the JWK spec, this array can contain one or more certs (it’s actually meant for X509 certs), and the remaining entries can contain the certificate chain. In the case of of Tyk this is irrelevant, but worth mentioning if you look at the JWKs of other providers.

You can use HMAC, Tyk will assume the key it gets from the JWK matches the method set in the API Definition, so HMAC is perfectly acceptable in this scenario. Tyk doesn’t check the kty field in this particular implementation - it is very raw (it’s why we’ve brought in full OIDC support elsewhere).

It’s a scan, so I don’t think using it with 6k+ keys is a good idea. My assumption was that you had a small number of IDP’s under your control that were authorised to generate tokens for client IDs and therefore the number of secrets to check would be much lower.

The validation chain in my head was:

  1. User logs into an authorized IDP with a client that has already authenticated with that IDP via OAuth or some other pre-vetting mechanism
  2. That IDP (e.g. Auth0) generates a JWT for the user, this is then passed to the client application
  3. Client application uses the JWT to gain access to the API
  4. The Gateway validates the JWT against it’s KID via a JWK entry

Let’s assume Auth0, it looks up the KID for Auth0, finds it in the JWK and then uses that to validate the JWT. Now Auth0 (a trusted provider), has generated a token for a client that it trusts (because somehow the client ID was registered with it via an API call under your control) and a user bearing this token, and a cryptographically valid signature has now come knocking.

This scenario is best with RSA as you’re using public keys, not shared HMACs so the signature can be trusted more, but yes - there is a speed difference in validating an RSA public key vs. an HMAC signature (it’s small, but it is there).

I think the 6k keys issue would need to be resolved at the JWT provider stage, so instead of each client have it’s own secret, the JWT providers are trusted, and the JWT providers validate the client IDs on JWT creation. Then, the provider that creates app-to-app tokens has a different secret to providers that generate JWTs for client apps, so they can be well segregated and then the only thing remaining is ensuring client validation (some kind of white list for the client ids).

Not a full solution I know, but it’s the best we can provide with the existing JWT implementation until full OIDC is released :-/

1 Like

What is the reason for only allowing one auth?

@polarbearyi No specific reason, when Tyk was first built it was built around one auth method per API, and although there’s been some requests for more auth methods, we haven’t seen it rise up enough in terms of interest to move up in the roadmap priorities.

If there is no specific reason, I hope that tyk will allow multi auth methods. If tyk release new auth method like openId or something like that and I want to use it, I have to make new api definition for it…isn’t it?

OpenID has been released already

And no you don;t need to re-create, you can switch the existing definition to OpenID, but all your old keys will not work, for obvious reasons since the auth mechanism will have changed.

Thanks your quick anwser.

The problem is the old keys are not working. My company is considering tyk professional edition for api gateway on micro service architecture. We want to deliver apis to the clients include apps. If old keys would not work, it would be big problem on apps. Because we have a policy that we have to service the apps at least 1 year, even the apps are older version.

And not just openID, whenever we want to deliver other auth methods to clients or clients want another method, we have to make a new api definition. Then we would have apis every auth methods. It would be inefficient.

I hope this issue will be considered positively.

This sounds a little risky in general, you’ll end up with an API that handles multiple auth methods, and have your client base sprinkled across different auth types and tokens. So yes, your API Definitions may fragment, but worse, your client auth base will fragment. I’m not sure which I’d rather manage :slight_smile:

However I take your point. It’s something we’re looking into.

Besides the example using JWK key types above, is there an alternate way to have auth settings vary per endpoint? I have an API definition where I would like the POST request to require auth, but the GET requests to not require authentication. The alternative I see is separating your endpoints into auth/no-auth API definitions, but the problem arises where API Definitions cannot use the same listen path. The main reason we would like to do this is for our api to be RESTful.

I’ve pasted an example definition below:

{
    "name": "TEST API",
    "api_id": "classes",
    "org_id": "test org API",
    "definition": {
        "location": "url-param",
        "key": "v"
    },
    "auth": {
        "auth_header_name": "authorization"
    },
    "version_data": {
        "not_versioned": true,
        "versions": {
            "Default": {
                "name": "Default",
                "expires": "2100-01-10 13:00",
                "use_extended_paths": true,
                "extended_paths": {
                    "ignored": [],
                    "white_list": [],
                    "black_list": [],
                    "url_rewrites": [
                        {
                            "path": "/api/v2/classes/{id}",
                            "method": "GET",
                            "match_pattern": "/api/v2/classes/(.*)",
                            "rewrite_to": "/v2/classes/$1"
                        },
                        {
                            "path": "/api/v2/classes/{id}",
                            "method": "POST",
                            "match_pattern": "/api/v2/classes/(.*)",
                            "rewrite_to": "/v2/classes/$1"
                        }
                    ]

                }
            }
        }
    },
    "proxy": {
        "listen_path": "/api/v{[1-2]}/classes/",
        "target_url": "WEB-SERVICE-URL",
        "strip_listen_path": true
    },
    "enable_batch_request_support": true,
    "CORS": {
        "enable": true,
        "allowed_origins": [
            "*"
        ],
        "allowed_methods": [
            "GET"
        ],
        "allowed_headers": [
            "authorization"
        ],
        "exposed_headers": [],
        "allow_credentials": false,
        "max_age": 0,
        "debug": false
    }
}

You can set paths in the endpoint designer to be ignored, in this case they will bypass authentication and pass right through

@Martin I put the url of JWK to the JWT secret field, but it is not work. And i have verified the token and pub keys themselves are valid. Here below is the exported API definition, I found the secret would become a hash string, is it the cause that tyk will treat the HASHED url as the pub key?

{
“id”: “5964544ae595230001212bf1”,
“name”: “test”,
“slug”: “test”,
“api_id”: “9a83a146c3d341e376aebe945a92070e”,
“org_id”: “59635dd4e5952300019e3fb8”,
“use_keyless”: false,
“use_oauth2”: false,
“use_openid”: false,
“openid_options”: {
“providers”: ,
“segregate_by_client”: false
},
“oauth_meta”: {
“allowed_access_types”: ,
“allowed_authorize_types”: ,
“auth_login_redirect”: “”
},
“auth”: {
“use_param”: false,
“param_name”: “”,
“use_cookie”: false,
“cookie_name”: “”,
“auth_header_name”: “Authorization”
},
“use_basic_auth”: false,
“enable_jwt”: true,
“use_standard_auth”: false,
“enable_coprocess_auth”: false,
“jwt_signing_method”: “rsa”,
“jwt_source”: “aHR0cDovL2xvY2FsaG9zdC9maWxlcy9pZHBfandrcy5qc29u”,
“jwt_identity_base_field”: “sub”,
“jwt_client_base_field”: “”,
“jwt_policy_field_name”: “kid”,
“notifications”: {
“shared_secret”: “”,
“oauth_on_keychange_url”: “”
},
“enable_signature_checking”: false,
“hmac_allowed_clock_skew”: -1,
“base_identity_provided_by”: “”,
“definition”: {
“location”: “header”,
“key”: “x-api-version”
},
“version_data”: {
“not_versioned”: true,
“versions”: {
“Default”: {
“name”: “Default”,
“expires”: “”,
“paths”: {
“ignored”: ,
“white_list”: ,
“black_list”:
},
“use_extended_paths”: true,
“extended_paths”: {},
“global_headers”: {},
“global_headers_remove”: ,
“global_size_limit”: 0,
“override_target”: “”
}
}
},
“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”: “”,
“port_data_path”: “”,
“target_path”: “”,
“use_target_list”: false,
“cache_timeout”: 60,
“endpoint_returns_list”: false
},
“recheck_wait”: 0
}
},
“proxy”: {
“preserve_host_header”: false,
“listen_path”: “/test/”,
“target_url”: “http://httpbin.org/”,
“strip_listen_path”: true,
“enable_load_balancing”: false,
“target_list”: ,
“check_host_against_uptime_tests”: false,
“service_discovery”: {
“use_discovery_service”: false,
“query_endpoint”: “”,
“use_nested_query”: false,
“parent_data_path”: “”,
“data_path”: “hostname”,
“port_data_path”: “port”,
“target_path”: “/api-slug”,
“use_target_list”: false,
“cache_timeout”: 60,
“endpoint_returns_list”: false
}
},
“disable_rate_limit”: false,
“disable_quota”: 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”: {}
}
},
“custom_middleware_bundle”: “”,
“cache_options”: {
“cache_timeout”: 60,
“enable_cache”: true,
“cache_all_safe_requests”: false,
“cache_response_codes”: ,
“enable_upstream_cache_control”: false
},
“session_lifetime”: 0,
“active”: true,
“auth_provider”: {
“name”: “”,
“storage_engine”: “”,
“meta”: {}
},
“session_provider”: {
“name”: “”,
“storage_engine”: “”,
“meta”: null
},
“event_handlers”: {
“events”: {}
},
“enable_batch_request_support”: false,
“enable_ip_whitelisting”: false,
“allowed_ips”: ,
“dont_set_quota_on_create”: false,
“expire_analytics_after”: 0,
“response_processors”: ,
“CORS”: {
“enable”: false,
“allowed_origins”: ,
“allowed_methods”: ,
“allowed_headers”: ,
“exposed_headers”: ,
“allow_credentials”: false,
“max_age”: 24,
“options_passthrough”: false,
“debug”: false
},
“domain”: “”,
“do_not_track”: false,
“tags”: ,
“enable_context_vars”: false
}

Ah, you need to add it via the API, not via the dashboard, the dashboard will always assume it’s a blob and encode it I think.

Manual JWK support is a bit weird in Tyk btw, which is why it isn’t documented, it’s a totally custom implementation which is non-standard.

If you want standards based JWK support, use OpenID Connect as an auth mechanism.

Regarding the custom JWKs…

Custom JWK support and JWT with Tyk (NOT OpenID Connect)

Create a fake JWK file that has the following format:

{
        "keys": [{
                "alg": "RS256",
                "kty": "RSA",
                "use": "sig",
                "x5c": [""],
                "n": "",
                "e": "AQAB",
                "kid": "12345",
                "x5t": "12345"
        }]
}

For each public key, you need a keys entry.

The kid and x5t values need to be the same as the kid claim you are going to use in your token. And the kty value must match the crypto method (RSA). You can safely ignore all the other fields.

You then need to encode your pem-encoded public key like this:

tyk@tyk-dev-env ~/jwk $ cat public.pem | base64 > b64.txt
tyk@tyk-dev-env ~/jwk $ cat b64.txt
...base64 data here...

Put this value into the first element of the x5c array, we only use the first value per key id in this method.

Once you have your two keys in place, put this JWK onto a web server that Tyk can see.

Now modify your API definition to use the URL instead of the raw public key as the value you add to the JWT source field.

Could you please explicit how to add it via the API? If not configure with dashboard, normally I just manually make changes from tables in mongodb.

The ‘n’ and ‘e’ are mandatory in JWK?

This conversation is being had twice, either have it here, or on GH (for reference, the GH issue is here):

(and FWIW I think the error isn’t the JWK, or the URL, your logs look like the file gets loaded fine, the issue is the JWK isn’t formatted correctly according to the non-standard way in which Tyk uses it - there’s a reason it’s not documented!)

It would ultimately be much more effective to use OpenID Connect instead of a JWK hack. There’s no guarantee we’re going to keep this capability around, because it doesn’t follow the JWK standard.