How to Use Demonstrating Proof-of-Possession (DPoP) Token With Kong and Keycloak
Last Christmas I published a blog post about using certificate bound access token with Kong and Keycloak. In the recent release of Kong Gateway 3.7.0.0, support has been added for another type of sender-constraining token called DPoP (Demonstrated Proof of Possession) tokens. In today’s post, I will explain what DPoP is, how to use it with Keycloak, and how to configure the Kong OpenID Connect plugin to authenticate DPoP tokens.
Let’s get started.
What is DPoP
I’ve already explained the reasons to use sender-constraining tokens in my last post. Compared to certificate-bound access tokens, DPoP-bound tokens are tied to a client-generated key that is held by the client, which making implementation easier since managing PKI can be challenging. DPoP helps prevent replay attacks by including the request URL, request method, and access token hash in the DPoP proof JWT payload. This informs the resource server which token the DPoP proof JWT should be used with, and specifies the exact method and URL for which this proof JWT is intended for.
The request flow is illustrated in the diagram below:
- Generate DPoP proof JWT for requesting a token from Keycloak.
- Send a request with the DPoP proof to Keycloak. If it is valid, Keycloak returns an access_token with the
cfn.jkt
claim, similar to the example below.1
2
3"cnf": {
"jkt": "00xoEKLML0EM1d1gqB9rBabpSwAG1DzhSIkXbFs-8is"
}, - Generate a new DPoP proof JWT and add
ath
claim based on the access_token. - Request send to resource server (Kong, in this case) with both access_token and the DPoP proof JWT.
DPoP proof JWT
DPoP introduces the concept of a DPoP proof, which is a JWT created by the client and sent with an HTTP request using the DPoP header field. Each HTTP request requires a unique DPoP proof. A valid DPoP proof demonstrates to the server that the client holds the private key used to sign the DPoP proof JWT. This enables authorization servers to bind issued tokens to the corresponding public key and enables resource servers to verify the key-binding of tokens it receives, preventing unauthorized entities from using the tokens.
To assist with testing, I have prepared the following Python script. Make sure you have the required libraries set up (you can use my nix template to set up the Python development environment). Save the code below to main.py
and run python main.py --url http://test
to generate an RSA key pair and the first DPoP proof JWT token.
Python library required:
- jwcrypto
- shortuuid
The script performs the following tasks:
- It has 3 flags
- –url: (Mandatory) Specify the URL to send your request to.
- –method: (Default: GET) Specify the method for your request.
- –ath: (Optional) (Optional) Pass in the access_token for the script to calculate and add the
ath
claim to the payload. If omitted, theath
claim will not be added.
- Generates RSA key pairs and saves them as private.json and public.json in the current directory if the files are not found.
- Creates a DPoP proof JWT with essential information, such as:
- Public key in the
jwk
claim in the header. - Request URL in the
htu
claim in the payload. - Request method in the
htm
claim in the payload.
- Public key in the
1 | from jwcrypto import jwk, jwt |
Some IDPs might require a nonce
in the payload returned in the DPoP-Nonce header. Make sure to add this in the payload if required.
Keycloak
Now that we know how to generate a DPoP proof, let’s set up Keycloak and obtain a token from it.
Installation
We will use Docker for easy installation. Since DPoP is still a technical preview feature, we need to enable it specifically using the --features=dpop
flag.
1 | services: |
Get Token
Once Keycloak is running, let’s try to get a DPoP token. Since Keycloak currently supports only public grant, we need to set up a user and configure a redirect URL on the client.
Then let’s create some variables.
1 | KEYCLOAK_HOST= |
Use Oauth2c
Since we are using the authorization code flow, it’s easier to use an application to send the required requests for us. oauth2c is a handy tool that helps run different OAuth flows in the terminal. It also handles generating DPoP proof JWTs for you.
Once the CLI is installed, run the following command. Use the private key you just generated (private.json
) as the signing key and specify the use of DPoP.
1 | oauth2c "https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}/.well-known/openid-configuration" \ |
This command will open your default browser for you to log in. After successfully logging in, you should receive a response similar to the one below.
1 | { |
Use cURL
AAlthough OAuth2c simplifies obtaining a DPoP token, let’s use cURL to understand what’s happening behind the scenes. We will reuse the command from this old post.
First, generate the authorization URL and access it in your browser:
1 | echo "https://$KEYCLOAK_HOST/realms/$KEYCLOAK_REALM/protocol/openid-connect/auth?client_id=$CLIENT_ID&response_type=code&redirect_uri=http://localhost/callback&scope=openid" |
You will be redirected back to http://localhost/callback
with a token in the URL, which should look like this:
1 | http://localhost/callback?session_state=a603b06f-4269-462b-9c3c-492153a4e196&iss=https%3A%2F%2Fkey.li.lan%3A9443%2Frealms%2Fdemo&code=58dcbb79-e760-457c-88b6-c8e2cc27251d.a603b06f-4269-462b-9c3c-492153a4e196.dacdb04e-9b41-483a-ab20-1d2d31e475b8 |
Extract the authorization code from the URL and save it to an environment variable:
1 | CODE=a04d2466-2475-4a28-be55-c3a29f1b2d39.a603b06f-4269-462b-9c3c-492153a4e196.dacdb04e-9b41-483a-ab20-1d2d31e475b8 |
Next, create the DPoP proof JWT and save it to a variable dpop
.
1 | dpop=$(python main.py --url https://$KEYCLOAK_HOST/realms/$KEYCLOAK_REALM/protocol/openid-connect/token --method POST) |
Now, send the token request using cURL:
1 | curl -sk --request POST \ |
If everything works correctly, you should receive a 200 response similar to this:
1 | { |
If you receive the following response, it means your DPoP proof is not active. I have added an arbitrary 5-second buffer to the DPoP proof JWT because Keycloak has a time limit.
1 | {"error":"invalid_dpop_proof","error_description":"DPoP proof is not active"} |
Let’s save the access token to a variable access_token
.
1 | access_token=eyJhbGciOiJSUzI1Nixxx... |
Kong
Now that we know how to obtain a DPoP token from Keycloak, let’s learn how to authenticate this token with the Kong OpenID Connect plugin.
Start Kong EE
For this demo, we’ll deploy Kong in DB-less mode. Assuming you have a valid Kong Enterprise license stored in the environment variable KONG_LICENSE_DATA
, you can start Kong Enterprise 3.5 in DB-less mode with the following command.
1 | docker run --rm --detach --name kong \ |
Prepare Kong config
Save the following config to /tmp/kong.yaml
and push it to Kong via the <admin_api>/config
endpoint.
1 | _format_version: "3.0" |
Validate DPoP
To authenticate with Kong, we need to create a new DPoP proof JWT. In the command below, I specify that the request will be sent to http://localhost:8000/test
using the HTTP GET method. This token will be used with the access_token
we just generated so we passed in the access_token
to include the ath
claim.
1 | kong_dpop=$(python main.py --url http://localhost:8000/test --method GET --ath $access_token) |
Now, send the request to Kong:
1 | curl --request GET \ |
If everything is set up correctly, you should receive a 200 response. The upstream service should receive the DPoP token in the authorization bearer header, and the DPoP proof will be removed.
That’s all I want to share with you today, see you next time.