Use Client Assertion With Kong OIDC Plugin and Okta

I’ve been trying a few IDPs and okta seems to have the most features and the easiest to use. Developers only need to register an account and start using it. Okta also supports private_key_jwt which is a huge plus compared to AzureAD or Auth0. Let me show you how to use it.

Prerequisites:

  • Kong Gateway (Enterprise)
  • OIDC server is running. (Keycloak in my example) If you are not sure how to use keycloa, you can check my previous post
  • I highly recommend reading this article to understand how client authentication works.

Create testing Service and Route

We need a service and route for our testing.

Create service

I am using httpbin as upstream server.

1
2
3
4
curl -X POST \
--url "http://<ADMIN_API>/services" \
--data "name=oidc-demo" \
--data "url=http://httpbin.org/anything"

Create Route

Next we will create a path /demo to access our service.

1
2
3
curl -X POST \
--url "http://<ADMIN_API>/services/oidc-demo/routes" \
--data 'paths[]=/demo'

Use Kong JWK

Okta’s website says both RSA and Elliptic Curve (EC) keys are supported. We can use below command to filter RSA and EC public key from Kong’s JWK.

1
curl -s http://<ADMIN_API>/openid-connect/jwks | jq '.keys |= map(select(.kty=="RSA" or .kty=="EC"))'

For this demo, I will only use the public keys that has signature algorithm as ES256.

1
curl -s http://<ADMIN_API>/openid-connect/jwks | jq '.keys |= map(select(.alg=="ES256"))'

We should get something similar as below.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"keys": [
{
"use": "sig",
"y": "taKDhagu38pcK9acgqZ5kwuwE8LssSXYDH2cq9tMEJo",
"crv": "P-256",
"kid": "h3hlm0AXuFS8frAb817kosAy947lGWHV287C5x1IoWs",
"kty": "EC",
"alg": "ES256",
"x": "h5nkM_J7s4NW8YJUnvrt_o3ZOI1geu0n0xBKa6-1d7c"
}
]
}

Create Okta client

We will use public key we got above to create this Okta client, below is a sample call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
curl --request POST \
--url https://<OKTA_ADMIN_DOMAIN>/oauth2/v1/clients \
--header 'Accept: application/json' \
--header 'Authorization: SSWS <OKTA_API_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"client_name": "client secret",
"response_types": [],
"grant_types": [],
"token_endpoint_auth_method": "private_key_jwt",
"application_type": "native",
"redirect_uris": [],
"jwks": {
"keys": [
{
"use": "sig",
"y": "taKDhagu38pcK9acgqZ5kwuwE8LssSXYDH2cq9tMEJo",
"crv": "P-256",
"kid": "h3hlm0AXuFS8frAb817kosAy947lGWHV287C5x1IoWs",
"kty": "EC",
"alg": "ES256",
"x": "h5nkM_J7s4NW8YJUnvrt_o3ZOI1geu0n0xBKa6-1d7c"
}
]
}
}'

For more information about creating client with private_key_jwt, please check okta site.

Please don’t forget to assign this new application to your users, otherwise you will get 401 errors.

Enable OIDC plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl --request POST \
--url https://<ADMIN_API>/plugins \
--header 'Content-Type: application/json' \
--data '{
"name": "openid-connect",
"config": {
"issuer": "https://<OKTA_DOMAIN>/oauth2/default",
"client_id": [
"<CLIENT_ID>"
],
"auth_methods": [
"password"
],
"client_auth": [
"private_key_jwt"
],
"client_alg": [
"ES256"
]
}
}'

Now we can visit the protected route.

1
2
3
curl --request GET \
--url https://<PROXY_NODE>/demo/anything \
--header 'Authorization: Basic base64(username:password)'

Use self generate keys

If we want to create our own (IMO this is a better idea) JWKs, we can use tools like openssl. I decided to use https://mkjwk.org for demonstration purpose. You can build your own CLI tool to use it locally. Let’s say the keys that we got today is below:

Full key (Include Private and public key):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"keys": [
{
"kty": "EC",
"d": "4PbzHut5BBd0v3xJ4-fHh1HDw4AmnHlhre6IgJir4CY",
"use": "sig",
"crv": "P-256",
"kid": "Z85ape3mBdhnq4In7vkpHtGN2ijDTFNOlk_eYoxteT8",
"x": "CvDq_1z-VrkVm3EbY1abXbI8cCRRVc6_31h4Xc4bFIk",
"y": "-WoxLtT2M5nlHKj2kr7F1z-Yu8usTTfVGwJHX03VC4U",
"alg": "ES256"
}
]
}

Public key:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"keys": [
{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"kid": "Z85ape3mBdhnq4In7vkpHtGN2ijDTFNOlk_eYoxteT8",
"x": "CvDq_1z-VrkVm3EbY1abXbI8cCRRVc6_31h4Xc4bFIk",
"y": "-WoxLtT2M5nlHKj2kr7F1z-Yu8usTTfVGwJHX03VC4U",
"alg": "ES256"
}
]
}

Create Okta client

We will use public key we generated above to create this Okta client, below is a sample call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
curl --request POST \
--url https://<OKTA_ADMIN_DOMAIN>/oauth2/v1/clients \
--header 'Accept: application/json' \
--header 'Authorization: SSWS <OKTA_API_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"client_name": "client secret",
"response_types": [],
"grant_types": [],
"token_endpoint_auth_method": "private_key_jwt",
"application_type": "native",
"redirect_uris": [],
"jwks": {
"keys": [
{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"kid": "Z85ape3mBdhnq4In7vkpHtGN2ijDTFNOlk_eYoxteT8",
"x": "CvDq_1z-VrkVm3EbY1abXbI8cCRRVc6_31h4Xc4bFIk",
"y": "-WoxLtT2M5nlHKj2kr7F1z-Yu8usTTfVGwJHX03VC4U",
"alg": "ES256"
}
]
}
}'

For more information about creating client with private_key_jwt, please check okta site.

Please don’t forget to assign this new application to your users, otherwise you will get 401 errors.

Enable OIDC plugin

The last step is to enable OIDC Plugin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
curl --request POST \
--url https://<ADMIN_API>/plugins \
--header 'Content-Type: application/json' \
--data '{
"name": "openid-connect",
"config": {
"issuer": "https://<OKTA_DOMAIN>/oauth2/default",
"client_id": [
"<CLIENT_ID>"
],
"auth_methods": [
"password"
],
"client_auth": [
"private_key_jwt"
],
"client_jwk": [
{
"kty": "EC",
"d": "4PbzHut5BBd0v3xJ4-fHh1HDw4AmnHlhre6IgJir4CY",
"use": "sig",
"crv": "P-256",
"kid": "Z85ape3mBdhnq4In7vkpHtGN2ijDTFNOlk_eYoxteT8",
"x": "CvDq_1z-VrkVm3EbY1abXbI8cCRRVc6_31h4Xc4bFIk",
"y": "-WoxLtT2M5nlHKj2kr7F1z-Yu8usTTfVGwJHX03VC4U",
"alg": "ES256"
}
],
"client_alg": [
"ES256"
]
}
}'

Now we can visit the protected route.

1
2
3
curl --request GET \
--url https://<PROXY_NODE>/demo/anything \
--header 'Authorization: Basic base64(username:password)'