Generate JWK to Pair With JWT Signer Plugin
If you’ve read my blog post about using JWT plugin, you would know that RSA public key is associated with a consumer object to validate JWT token passed in. Although it works perfectly fine, there are two problems you might encounter because the public keys are attached to consumer objects.
- Users can’t do consumer mapping which means rate limiting, ACL control is not possible.
- If users need to rotate keys, they must update these consumer objects with the new public keys.
If you are using Kong enterprise, one method to solve these issues is to host users information on an OIDC provider and use OIDC plugin. The other method is to use JWT singer plugin.
JWT signer plugin can be used to validate token issued by IDP (Identity Provider) and re-sign it with Kong’s private key. It can also be used to validate opaque token against IDP’s introspection endpoint and sign a JWT token with the information returned. If user does not use IDP, they can create key pairs and sign their own JWTs to validate clients. Because we have control of what’s in the payload, we can do consumer mapping as well.
In this post, I will show you how to achieve this.
Let’s begin.
If you already know how to generate JWKs and create JWTs, you can start from self host JWKs.
Generate Keys
Tools I will use:
- OpenSSL -> for Key generation
- pem-to-jwk -> for converting PEM file to JWK
- jq -> for adding info to JWK
I’ve prepared a script, PLEASE REVIEW SCRIPT BEFORE RUNNING IT! This script generates both RSA and ECC key pairs.
The only reason I generate these keys this way because JWT CLI only supports PKCS8 EC keys, see this issue for more detail. If you only use RSA algorithm, you can use openssl to generate keys easily.
1 | /bin/bash <(curl -fsSL 'https://gist.githubusercontent.com/liyangau/51bef7735a7da219ced69c6447353f7b/raw') |
This script will generate 4 files
- jwt-rsa-private.p8
- jwt-rsa-public.pem
- jwt-ecc-private.p8
- jwt-ecc-public.pem
These file names should be self-explanatory. Files with file extension p8
are private keys in DER format. The reason I use DER format because the tool (jwt-cli) I use to generate JWT token accepts private keys in DER format only.
Once the keys are generated, let’s create our JWKs.
Create JWK
We can generate JWKs from our Public keys as below.
RSA key
1
2cat jwt-rsa-public.pem | docker run -i danedmunds/pem-to-jwk:latest \
--jwks-out | jq '.keys[] += {alg:"RS256"}'We should get an result similar to below
1
2
3
4
5
6
7
8
9
10
11{
"keys": [
{
"kty": "RSA",
"kid": "2BuwZaYxqLhhTMX80Wt-vo4iLb4ubHPJY2oHwv6ie1c",
"n": "15-LspeeSGJhrVz1-Q2WZmv0B8dyQ9pPEbA68WSkkJuCUEqO-E29_wCqrLJ8q0_MvC7-6silS-XkmxDMXQFwtLD-Mf4_MOJwsHjywQXDn2wQj-qT57kqsYPDVO0fh-Yaylqmf8v-nEN5vh_w87MOozBpogw1yVnLZc_SNj200LNcEnFRpESqRuwM_ZzIFF6rccDQlku_heA6n9odDDAMkR7tmN-f8NHZ1LKk4Ugxfqt9QL1qYSLH_D14x_DQ1iHSCGay4bNgyeFc-Riq-5iEmCmrPwfRg3H_OJ_MJ1Qjh2J2ErRcdPh8bc2PZvCU1t20S4nyAU39_S73PPgaviaUTQ",
"e": "AQAB",
"alg": "RS256"
}
]
}ECC Key
1
2cat jwt-ecc-public.pem | docker run -i danedmunds/pem-to-jwk:latest \
--jwks-out | jq '.keys[] += {alg:"ES256"}'We should get result similar to below
1
2
3
4
5
6
7
8
9
10
11
12{
"keys": [
{
"kty": "EC",
"kid": "sXBzQf92Err7qnFyR-ywRQ4xT9to06ptWmexn-wiFaY",
"crv": "P-256",
"x": "Q4K0pd4VYsQyH0j6nM5oxTJxkPcgJ0e8bQS54SHUW-k",
"y": "Ws1hhKAVT8YlYV2wWHkkHzyiqSaEyXm3AsXviuTEAOg",
"alg": "ES256"
}
]
}Let’s combine both keys to one keyset
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
"keys": [
{
"kty": "RSA",
"kid": "2BuwZaYxqLhhTMX80Wt-vo4iLb4ubHPJY2oHwv6ie1c",
"n": "15-LspeeSGJhrVz1-Q2WZmv0B8dyQ9pPEbA68WSkkJuCUEqO-E29_wCqrLJ8q0_MvC7-6silS-XkmxDMXQFwtLD-Mf4_MOJwsHjywQXDn2wQj-qT57kqsYPDVO0fh-Yaylqmf8v-nEN5vh_w87MOozBpogw1yVnLZc_SNj200LNcEnFRpESqRuwM_ZzIFF6rccDQlku_heA6n9odDDAMkR7tmN-f8NHZ1LKk4Ugxfqt9QL1qYSLH_D14x_DQ1iHSCGay4bNgyeFc-Riq-5iEmCmrPwfRg3H_OJ_MJ1Qjh2J2ErRcdPh8bc2PZvCU1t20S4nyAU39_S73PPgaviaUTQ",
"e": "AQAB",
"alg": "RS256"
},
{
"kty": "EC",
"kid": "sXBzQf92Err7qnFyR-ywRQ4xT9to06ptWmexn-wiFaY",
"crv": "P-256",
"x": "Q4K0pd4VYsQyH0j6nM5oxTJxkPcgJ0e8bQS54SHUW-k",
"y": "Ws1hhKAVT8YlYV2wWHkkHzyiqSaEyXm3AsXviuTEAOg",
"alg": "ES256"
}
]
}
Let’s save it as jwk.json
and then we can host this file on a server.
Self host JWKs
I will use json-server to host my JWK and put this inside the same docker network as Kong.
Create Network
I will run all my containers in network called kong-net
.
1 | docker network create kong-net |
Start JSON server container
I am mapping port 3000 of host machine to this container to make sure it works.
1 | docker run --detach --name jwk \ |
Start Kong Enterprise with Postgres
If you haven’t read my post about different kong deployment methods on docker, I recommend you to check it out.
I will deploy Kong with Postgres database. The commands I use are very similar to what’s on that post except adding a license and a couple security related environment variables.
Create Postgres container
Let’s create a postgres container with below command. I call this container kong-db
and create database kong
, user kong
and user password kong
when it starts.
1 | docker run --detach --name kong-db \ |
Database Bootstrap
We need to bootstrap the database first. This means kong needs to create some tables in the database before it starts. I’ve set KONG_PASSWORD=kong
in this process. This will create an initial super-admin kong_admin in the database with kong
as its RBAC token and Kong manager admin password. Although we don’t use Kong manager today, I want to enable RBAC token for security reasons.
1 | docker run --rm --name kong-migrations \ |
Start Kong container
I have set KONG_ENFORCE_RBAC=on
here to force users to present RBAC token for making admin api calls.
1 | docker run --detach --name kong \ |
Test Plugin
As usual I will use httpbin as our upstream server.
Create Service
1 | curl -X POST \ |
Create Route
1 | curl -X POST \ |
Create Consumer
This will create a consumer with username demo
.
1 | curl -X POST \ |
When we make a call, we should see 200 response.
1 | curl -X GET --url http://localhost:8000/test/anything -I |
Enable JWT Signer plugin
I enable JWT signer plugin with below settings.
config.channel_token_optional=true
will make channel token optional. We are passing 1 JWT token only.config.access_token_jwks_uri=http://jwk:3000/db
is to load JWK (which is used to verify JWT) from our JWK server deployed earlier.config.access_token_consumer_claim=username
will map the value of JWK claim keyusername
to Kong consumer’s username. Access will be denied if no match found.
1 | curl --request POST \ |
Let’s access the route again and our request should be rejected.
1 | curl -X GET --url http://localhost:8000/test/anything -I |
Generate JWT token
Because my keysets only has 1 RSA and 1 EC key, Kong knows which public to use for validating the JWT token I passed in.
If you use multiple RSA or EC keys, you need to include thekid
in the jwt command to generate your JWT token.
I think it might be a good habbit to always includekid
when generating tokens.
To show you the consumer mapping, I will include username=demo
in payload.
Generate Token with RSA private key
1
2
3
4jwt encode --alg=RS256 \
--kid=2BuwZaYxqLhhTMX80Wt-vo4iLb4ubHPJY2oHwv6ie1c \
--exp=300s --iss=kong-demo \
[email protected] '{"iss":"demo","username":"demo"}'Generate Token with ECC private key
1
2
3
4jwt encode --alg=ES256 \
--kid=sXBzQf92Err7qnFyR-ywRQ4xT9to06ptWmexn-wiFaY \
--exp=300s --iss=kong-demo \
[email protected] '{"iss":"demo","username":"demo"}'
Let’s say the JWT we got is below
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6InNYQnpRZjkyRXJyN3FuRnlSLXl3UlE0eFQ5dG8wNnB0V21leG4td2lGYVkifQ.eyJleHAiOjE2NDUwNjcyNjYsImlhdCI6MTY0NTA2Njk2NiwiaXNzIjoiZGVtbyIsInVzZXJuYW1lIjoiZGVtbyJ9.LJgS1vkTGdvKwQWwxeVP0sdQK8P_8y4HtC7YUO3oKCm49xjZPDtsBVa61EheEhay0_RvzfBqlHTedjULiRuz8g |
We can make our request again with
1 | curl -X GET --url http://localhost:8000/test/anything -I \ |
We should get HTTP/1.1 200 OK
again.
Rotate Keys
If you need to rotate keys from time to time. You just need to repeat Generate Keys step above and put the new keyset on jwk.json
file.
Once it is done, you can rotate Keyset on Kong.
Get JWK ID
1
2
3curl --request GET --silent \
--url http://localhost:8001/jwt-signer/jwks \
--header 'Kong-Admin-Token: kong' | jq -r '.data[] | select(.name=="http://jwk:3000/db") | .id'Let’s say the id we get is
ebe5c286-6a7e-4f95-b6cb-cc7a6463b0c7
.Load new JWK from server
1
2
3curl --request POST --silent \
--url http://localhost:8001/jwt-signer/jwks/ebe5c286-6a7e-4f95-b6cb-cc7a6463b0c7/rotate \
--header 'Kong-Admin-Token: kong'
That’s all I want to show you today, see you on the next one.