OIDC configuration with JWK


#1

I am trying to configure the OIDC to my APIs. It’s expected to be authorized by user requests with a JWT token like Bearer xxxxxx==.

Here below is my test code with Python, I have created two api endpoints,

  • /test/ , by normal JWT auth, and I use it to verify the JWT I generated is valid.
  • /test2/, configured with OIDC, this is the target URL.

Both of the api endpoints were linked with a same policy.

import jwt

pub_key = """-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOn8rRVbujDfvL08ozx6nSS0n5D2GYB5
YQzGdw+lpHceztYpH5EpbabigH2EC3EmYU7dStlEvoZZujH+53Tr7bECAwEAAQ==
-----END PUBLIC KEY-----"""
prv_key = """-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAOn8rRVbujDfvL08ozx6nSS0n5D2GYB5YQzGdw+lpHceztYpH5Ep
babigH2EC3EmYU7dStlEvoZZujH+53Tr7bECAwEAAQJAJRIKHfslWX7o+RY1Smym
nt0a9q12xtr0HEEJNeppTxG94zF4LOxieqeHjYMpkcvT0cVIMWdCUgjffI0V9gcE
AQIhAPpFTgSRmiGxBhstwjlvP+1UqA6+vmWHOGLYrjrAUF9xAiEA71fxFtiwRq7g
ghVtxF+0jzUIrs9DtcsaGq94/6tJ0kECIQDtJExn0dt9fzLs/+/g4kRtyuCvkBfz
Niy8pNp6uw2tUQIgUUEQxwnPZl+uNltiBX3cRZXimb4hpZELDq4trNaQ3EECIAD+
8UdQm3xdnaFWwNUlkuNxpVbq9mRvl/ibZhEeXPeS
-----END RSA PRIVATE KEY-----"""

headers = {
    "alg": "RS256",
    "typ": "JWT"
}

# The policy_id regarding to tyk
kid = '596728c0ad1db8000188f9f5'
aud = "http://10.0.1.145/files/idp_jwks.json"
payload = {
    "sub": "0", 
    "azp": "iPhone-App",
    "iat": 1482071636,
    "exp": 2167793995,
    "kid": kid
}

payload.update({
    "iss"   : "accounts.google.com",
    "aud"   : aud
})
print('Payload: ',payload)
encoded = jwt.encode(payload, prv_key, algorithm='RS256', headers=headers)

decoded = jwt.decode(encoded, pub_key, algorithms=['RS256'], audience=aud)
print("Verify Decoded: ", decoded == payload)

JWT = 'Bearer ' + encoded.decode()
print('JWT: ', JWT)

import base64
b64_key = base64.b64encode(pub_key.encode())
print("Base64 PUB key: ", b64_key.decode())

headers = {'Authorization': JWT}

import requests
# A basic API endpoint with normal JWT authentication method.
jwt_verify_url = 'http://localhost/test/'
r1 = requests.get(jwt_verify_url, headers=headers)

print(" ")
if r1.status_code == 200:
    print("JWT is verified OK!")
else:
    print("JWT is not valid!!!")

# The target endpoint to test OIDC
api_url = 'http://localhost/test2/'
r = requests.get(api_url, headers=headers)

if r.status_code != 200:
    print('Status code: {}, Response: {}'.format(r.status_code, r.text))
else:
    print("Status code: {}, Request OK!".format(r.status_code))

Output:

Payload:  {'sub': '0', 'azp': 'iPhone-App', 'iat': 1482071636, 'exp': 2167793995, 'kid': '596728c0ad1db8000188f9f5', 'iss': 'accounts.google.com', 'aud': 'http://10.0.1.145/files/idp_jwks.json'}
Verify Decoded:  True
JWT:  Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwIiwiYXpwIjoiaVBob25lLUFwcCIsImlhdCI6MTQ4MjA3MTYzNiwiZXhwIjoyMTY3NzkzOTk1LCJraWQiOiI1OTY3MjhjMGFkMWRiODAwMDE4OGY5ZjUiLCJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXVkIjoiaHR0cDovLzEwLjAuMS4xNDUvZmlsZXMvaWRwX2p3a3MuanNvbiJ9.ZZdGid4soh7VGAEWHrqpfpAv_gEHL7mbhbreZVh6qDXuXEEh5w1CEfqqUp5wkwMcfWKB05pljZKyFLT56J0hpA
Base64 PUB key:  LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBT244clJWYnVqRGZ2TDA4b3p4Nm5TUzBuNUQyR1lCNQpZUXpHZHcrbHBIY2V6dFlwSDVFcGJhYmlnSDJFQzNFbVlVN2RTdGxFdm9aWnVqSCs1M1RyN2JFQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
 
JWT is verified OK!
Status code: 403, Response: {
    "error": "Key not authorised"
}

Error log:

tyk_gateway_1    | time="Jul 13 10:08:46" level=warning msg="JWT Invalid: Validation error. Jwt token validation failed." 
tyk_gateway_1    | time="Jul 13 10:08:46" level=warning msg="Attempted access with invalid key." key="[JWT]" 
tyk_gateway_1    | time="Jul 13 10:08:46" level=error msg="request error: Key not authorised" api_id=c2b8938b618242426e87327e7332b8d2 org_id=59672879ad1db8000188f9ee path="/" server_name="http://httpbin.org/" user_id= user_ip=172.19.0.1

The log indicates that the token validation failed. Since the URL of JWK was specified in the aud field, is it the correct way configure OIDC with JWK to work?

The referred JWK is

{
	"keys": [
		{
			"alg": "RS256",
			"kty": "RSA",
			"use": "sig",
			"x5c": ["LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBT244clJWYnVqRGZ2TDA4b3p4Nm5TUzBuNUQyR1lCNQpZUXpHZHcrbHBIY2V6dFlwSDVFcGJhYmlnSDJFQzNFbVlVN2RTdGxFdm9aWnVqSCs1M1RyN2JFQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==",
			"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ4AhQ6FGspvMAnBmclAzydVB35i/MJe\nDq+OB5di7YLy3VcH66MJ0NSnEy/s55hgcQQ+IozJK4UTyAyGwRGVxn8CAwEAAQ=="
			],
			"kid": "596728c0ad1db8000188f9f5",
			"x5t": "596728c0ad1db8000188f9f5"
		}
	]
}

#2

Are you actually using OIDC?
What are you using for your IDP (e.g. auth0, keycloak, google)?

Can you share your api definition?


#3

Please see api definition here.

{
    "id": "59672907ad1db8000188f9f6",
    "name": "test2",
    "slug": "test2",
    "api_id": "c2b8938b618242426e87327e7332b8d2",
    "org_id": "59672879ad1db8000188f9ee",
    "use_keyless": false,
    "use_oauth2": false,
    "use_openid": true,
    "openid_options": {
        "providers": [
            {
                "issuer": "accounts.google.com",
                "client_ids": {
                    "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0gTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBT244clJWYnVqRGZ2TDA4b3p4Nm5TUzBuNUQyR1lCNSBZUXpHZHcrbHBIY2V6dFlwSDVFcGJhYmlnSDJFQzNFbVlVN2RTdGxFdm9aWnVqSCs1M1RyN2JFQ0F3RUFBUT09IC0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==": "596728c0ad1db8000188f9f5",
                    "aHR0cDovL2xvY2FsaG9zdC9maWxlcy9pZHBfandrcy5qc29u": "596728c0ad1db8000188f9f5",
                    "aHR0cDovLzEwLjAuMS4xNDUvZmlsZXMvaWRwX2p3a3MuanNvbg==": "596728c0ad1db8000188f9f5",
                    "aVBob25lLUFwcA==": "596728c0ad1db8000188f9f5"
                }
            }
        ],
        "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": ""
    },
    "use_basic_auth": false,
    "enable_jwt": false,
    "use_standard_auth": false,
    "enable_coprocess_auth": false,
    "jwt_signing_method": "",
    "jwt_source": "",
    "jwt_identity_base_field": "",
    "jwt_client_base_field": "",
    "jwt_policy_field_name": "",
    "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": "/test2/",
        "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
}

#4

So you have google as your IDP, but you are generating the token yourself with your own private key. The way OIDC works is that Tyk will call the IDP discovery URL (So the google accounts service) to verify the token with the kid against their JWK document. That means you need to generate the token using Google’s auth service (e.g. Google+).

Also, in OIDC the aud claim is the client ID of the application doing the requesting, so it would be an OAuth client ID or similar.

In your APi definition, for some reason you have put a public key and random UR:L’s as the client IDs?

It think there’s a misunderstanding on how OIDC works, here’s some reading:


#5

@Martin Thanks for your advice. Now I created a pseudo OP by creating a openid-configuration file. It was actually a JSON inside, but tyk prompt below error that cannot decode the file.

tyk_gateway_1    | time="Jul 14 08:47:05" level=warning msg="JWT Invalid: Validation error. Validation error. Failure while decoding the configuration retrived from endpoint http://10.0.1.145/.well-known/openid-configuration." 
tyk_gateway_1    | time="Jul 14 08:47:05" level=warning msg="Attempted access with invalid key." key="[JWT]" 
tyk_gateway_1    | time="Jul 14 08:47:05" level=error msg="request error: Key not authorised" api_id=ce150033826c4e7174d28fb87b3ede7a org_id=59687c2a4172510001964c16 path="/" server_name="http://httpbin.org/" user_id= user_ip=172.19.0.1 

And the file can be accessed,

[email protected]:tyk $curl -k -I http://127.0.0.1/.well-known/openid-configuration
HTTP/1.1 200 OK
Server: openresty/1.11.2.4
Date: Fri, 14 Jul 2017 08:46:53 GMT
Content-Type: application/octet-stream
Content-Length: 1468
Last-Modified: Fri, 14 Jul 2017 08:08:56 GMT
Connection: keep-alive
ETag: "59687c18-5bc"
Content-Type: application/json
Content-Type: charset=utf-8
Accept-Ranges: bytes

The OpenID configuration

// 20170714151942
// https://10.0.1.145/.well-known/openid-configuration

{
  "issuer": "http://10.0.1.145/",
  "authorization_endpoint": "http://10.0.1.145/authorize",
  "token_endpoint": "http://10.0.1.145/oauth/token",
  "userinfo_endpoint": "http://10.0.1.145/userinfo",
  "mfa_challenge_endpoint": "http://10.0.1.145/mfa/challenge",
  "jwks_uri": "http://10.0.1.145/.well-known/jwks.json",
  "registration_endpoint": "http://10.0.1.145/oidc/register",
  "scopes_supported": [
    "openid",
    "profile",
    "offline_access",
    "name",
    "given_name",
    "family_name",
    "nickname",
    "email",
    "email_verified",
    "picture",
    "created_at",
    "identities",
    "phone",
    "address"
  ],
  "response_types_supported": [
    "code",
    "token",
    "id_token",
    "code token",
    "code id_token",
    "token id_token",
    "code token id_token"
  ],
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "HS256",
    "RS256"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "aud",
    "auth_time",
    "created_at",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "identities",
    "iss",
    "name",
    "nickname",
    "phone_number",
    "picture",
    "sub"
  ]
}

Any idea about the decoding issue?


#6

You could use a standard OIDC provider instead of rolling your own?

Not really in the scope of this forum to help build an OIDC Provider - so can’t offer any advice on that.

But just a shot in the dark: have you linted your json file? JSON doesn’t allow comments…