Securing OAuth2 Tokens using DPoP with WSO2 Identity Server

Chamath
Identity Beyond Borders
6 min readJun 12, 2022

--

Demonstration of Proof-of-Posession (DPoP) is an application-layer Proof-of-Possession (PoP) mechanism that simply binds an OAuth2 token to the client that recieves it. It is an additional security mechanism for the token generation which overcomes the issue of bearer token which will not validate between who requested the token and who is actually using the token for the access of a particular resource. Although its specification OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP) is in draft state, it has been gaining more and more traction due to its simplicity and usability, especially in single page application authorization flows. And hence many identity vendors have already started supporting it.

In today’s article, we are going to see how you can secure your OAuth2 tokens using DPoP with the WSO2 Identity Server. For this article, we will be using the WSO2 Identity Server 5.11 which is the latest available release for WSO2 Identity Server.

If you are interested in watching a video demonstration of the content of this article, you can find the recording I did for #IdentityIn15 session below :)

The basic DPoP flow that we would be looking at would be as follows:

+--------+                                        +---------------+
| |--(A)---- Token Request --------------->| |
| Client | (DPoP Proof) | Authorization |
| | | Server |
| |<-(B)---- DPoP-bound Access Token ------| |
| | (token_type=DPoP) +---------------+
| |
| |
| | +---------------+
| |--(C)---- DPoP-bound Access Token ----->| |
| | (DPoP Proof) | Resource |
| | | Server |
| |<-(D)---- Protected Resource -----------| |
| | +---------------+
+--------+

(A) The client will send a token request to the Authorization Server (AS). Along with it, the client would send something called a “DPoP proof JWT” to the AS.

(B) The AS would validate the DPoP Proof JWT (This would be a signed JWT). Get the public key from the DPoP Proof JWT and bind it to the token that is to be issued. Hence the name; DPoP-bound Access Token.

(C) When the client wants to access a resource on the Resource Server (RS), it would send the access token obtained in (B), and along with that, it would also include a newly generated DPoP proof JWT that is meant for the RS.

(D) The resource server would validate the token, the binding between the DPoP proof JWT and the token, and honour the request.

Building the DPoP extension for WSO2 Identity Server

DPoP is currently available as an extension for the WSO2 Identity Server at https://github.com/wso2-extensions/identity-oauth-addons/tree/master/component/org.wso2.carbon.identity.dpop

To install this extension, we must first build the artifacts. For this, we are going to require Java 8+ and Maven 3.6+ running on our machine. The following steps outline how to build the DPoP artifacts.

  1. Clone the repository identity-oauth-addons
git clone https://github.com/wso2-extensions/identity-oauth-addons.git

2. Navigate to the component org.wso2.carbon.identity.dpop

cd $identity-oauth-addons/components/org.wso2.carbon.identity.dpop

3. Compile the code and build the module artifacts using maven.

mvn clean install

After a successful build, you should find the artifact, org.wso2.carbon.identity.dpop-x.x.x-SNAPSHOT.jar in the directory, components/org.wso2.carbon.identity.dpop/target.

Installing the Extension to the WSO2 Identity Server

  1. If you don’t have a running setup of WSO2 Identity Server already, download the WSO2 Identity Server and extract it into your local directory (IS_HOME).
  2. Copy the org.wso2.carbon.identity.dpop-2.4.x-SNAPSHOT.jar that we built in the previous section into the <IS_HOME>/repository/components/dropins directory.
  3. Open the deployment.toml in the <IS_HOME>/repository/confdirectory, add the following configurations, and save.
[[event_listener]]
id = "dpop_listener"
type = "org.wso2.carbon.identity.core.handler.AbstractIdentityHandler"
name="org.wso2.carbon.identity.dpop.listener.OauthDPoPInterceptorHandlerProxy"
order = 13
enable = true
properties.header_validity_period = 90
properties.skip_dpop_validation_in_revoke = "true"

[[oauth.custom_token_validator]]
type = "dpop"
class = "org.wso2.carbon.identity.dpop.validators.DPoPTokenValidator"

Configuring the WSO2 Identity Server

Since the DPoP extension is now installed, let’s create a service provider with OAuth2 Inbound Authentication configurations to enable the DPoP token binding.

  1. Navigate to <IS_HOME>/bin using the command prompt and start the server.
    Linux --> sh wso2server.sh
    Windows --> wso2server.bat

2. Sign in to the Management Console.

3. On the Main menu, click Identity -> Service Providers -> Add.

4. Fill in the Service Provider Name and Description (optional) of the service provider as follows.

Add new service provider

5. Click Register to add the new service provider.

6. Next, navigate to Service Providers -> List -> demo-app -> Edit -> inbound authentication configuration ->OAuth/OpenID Connect Configuration -> Configure.

7. Select Access Token Binding Type -> DPoP Based and enable validate token bindings option.

Setting Access Token Binding Type

8. Finish the service provider configurations by clicking on add .

Generating DPoP tokens

As illustrated in the basic DPoP flow diagram above, the client must send a DPoP proof along with the token request, and with the request to the resource server. This DPoP proof is a JWT and should be built in accordancewith the section 4 of the specification.

The following is a sample DPoP proof JWT as given in the specification.

  {
"typ":"dpop+jwt",
"alg":"ES256",
"jwk": {
"kty":"EC",
"x":"l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y":"9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
"crv":"P-256"
}
}
.
{
"jti":"-BwC3ESc6acc2lTc",
"htm":"POST",
"htu":"https://server.example.com/token",
"iat":1562262616
}

You can create a simple executable to build the DPoP proof JWTs for your authentication scenarios. You can refer to the https://github.com/chamathns/dpop-client when implementing a client for generating the DPoP proof JWTs. This is what I will be using to generate DPoP proof JWTs for this configuration.

Few things to note:

The JWT header typ should be set to "dpop+jwt".

The JWT claimset must at least include the following claims:

jti: Unique identifier for the DPoP proof JWT.
htm: The HTTP method of the request to which the JWT is attached.
htu: The HTTP request URI.
iat: Creation timestamp of the JWT

Trying it Out

Obtaining a DPoP-bound Access Token

For trying out the configuration, I would first obtain a DPoP bound access token from the /token endpoint of WSO2 Identity Server.

When calling the /token endpoint, we need to add a separate header to our request called dpop and attach a DPoP proof JWT generated for the /token request.

The payload of the DPoP proof JWT for the /token endpoint request should be similar to:

{
"htm": "POST",
"htu": "https://localhost:9443/oauth2/token",
"iat": 1654013960,
"jti": "ab8520e5-7796-44f0-877d-293f3933da81"
}

sample request:

curl --location --request POST 'https://localhost:9443/oauth2/token' --header 'Content-Type: application/x-www-form-urlencoded' --header 'dpop: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2IiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwia2lkIjoiZGQ4MThkMmItYmZmMy00MjM4LTljZjEtM2YxY2NiOWNlNDRkIiwibiI6IjFSYlZxZzhZZlBEZFo0UTBGSUh2WUZCbTEyQlVLNUgtbmdoY1lGb1A0QkQ2eVFpNWtrSVpuZGRnY3NJdVdGTEc5MlBkdmJsVXVZeldLVmE3WXIyTEJkdk1zeUpwYzVhb2Q5bmN3d1dPY0ktSnI0eko0MHdTNVY2TWhicHNLMktrUk0xT05XY1JKUkFTVzZxd1hPWlV4QVFVTC00dHJPeVNFN2RxSXFNbEdBV19WNUZUaElrUHNYZElDd3ZYNEQ5WmluYklxUGs1aEVwR0hZWVRZSzFYTFFucHB3NFFJaWNabFBwMVlheGFETzQ1MXpJdlZLbkg3VWxGNkJ1R0swX2dZM1cyNU1PTG9abU9QcWFLd21oMlNNU3N1WF8zdkMtT2ppMFNLZHA5VFdiUjVyODhZeHdDX3lEbmxCaVJWQmJVeWRMRFo5LXprNFVZVm0yTjZlTWh6USJ9fQ.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6XC9cL2xvY2FsaG9zdDo5NDQzXC9vYXV0aDJcL3Rva2VuIiwiaWF0IjoxNjU0MDEzOTYwLCJqdGkiOiJhYjg1MjBlNS03Nzk2LTQ0ZjAtODc3ZC0yOTNmMzkzM2RhODEifQ.JEaVRDB2OnzjUWP4P80SQT6oTTGDaj6GKH0au4TpSxiqadY4ZMFvy1QD_j9TOh5o4xn_E6qlrsNu01CarZpH2KTnGKJlJd3x_L-FLEsw1l9q1aAzdtJYpW0ZgdTKGDXD_iLRQEAgrt1KpvHz3XvZBsWO2e_MRUvldBSmXuwSO5uu9l0FmYy3kqoEozPWt8ZreX2UJkluURq6ZhQ8EiyWijNh1n2PSNHdnh36m49bZExHgQFh28eV1hvX5ppwdIFN_Cf8VexpMfodqb4UQva5h7_VTc56plrDRTqKSX0bfm_9wYyLGYSKKxCUTWnEKWHI2DNwVbthoiw9oH41p1dkQg' --header 'Authorization: Basic cGJlQmVMdU5xTXNfMzRvWVNVcHlRREVoa1ZRYTpsVUlYbVMyR2J3aDk3S3E1ZWwwMklmd09EWm9h' --data-urlencode 'grant_type=password' --data-urlencode 'username=admin' --data-urlencode 'password=admin' --data-urlencode 'scope=openid'

The response should be a 200 and you should find access token in the response body.

Accessing a Resource with a DPoP-bound Token

For the second part of this configuration, we will try to access the /scim2/users endpoint as our resource endpoint.

For this request, we would attach the previously obtained DPoP-bound access token in the Authorization Header, and add a separate dpop header containing a DPoP proof JWT.

The payload of the DPoP proof JWT for the /scim2/users endpoint request should be similar to:

{
"htm": "GET",
"htu": "https://localhost:9443/scim2/Users",
"iat": 1654014046,
"jti": "cd0bccc6-7b1e-483d-aed0-61b09bb443a2"
}

sample request:

curl --location --request GET 'https://localhost:9443/scim2/Users' --header 'accept: application/scim+json' 
--header 'Authorization: DPoP 1942f95e-060e-358f-bddc-7082843dde63'
--header 'DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2IiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwia2lkIjoiZGQ4MThkMmItYmZmMy00MjM4LTljZjEtM2YxY2NiOWNlNDRkIiwibiI6IjFSYlZxZzhZZlBEZFo0UTBGSUh2WUZCbTEyQlVLNUgtbmdoY1lGb1A0QkQ2eVFpNWtrSVpuZGRnY3NJdVdGTEc5MlBkdmJsVXVZeldLVmE3WXIyTEJkdk1zeUpwYzVhb2Q5bmN3d1dPY0ktSnI0eko0MHdTNVY2TWhicHNLMktrUk0xT05XY1JKUkFTVzZxd1hPWlV4QVFVTC00dHJPeVNFN2RxSXFNbEdBV19WNUZUaElrUHNYZElDd3ZYNEQ5WmluYklxUGs1aEVwR0hZWVRZSzFYTFFucHB3NFFJaWNabFBwMVlheGFETzQ1MXpJdlZLbkg3VWxGNkJ1R0swX2dZM1cyNU1PTG9abU9QcWFLd21oMlNNU3N1WF8zdkMtT2ppMFNLZHA5VFdiUjVyODhZeHdDX3lEbmxCaVJWQmJVeWRMRFo5LXprNFVZVm0yTjZlTWh6USJ9fQ.eyJodG0iOiJHRVQiLCJodHUiOiJodHRwczpcL1wvbG9jYWxob3N0Ojk0NDNcL3NjaW0yXC9Vc2VycyIsImlhdCI6MTY1NDAxNDA0NiwianRpIjoiY2QwYmNjYzYtN2IxZS00ODNkLWFlZDAtNjFiMDliYjQ0M2EyIn0.OGeecbc5dieFlu_qw0D-_YOs-KYyHi-SmKDQGVSENwBhL8Ux-mDqjNvAKTZJ4FL2p8182xQHU-KPQpJXX9TqWdpA5Jw2Wq-zkykRW_qOwOa_hLTMwFKLzWTcgYHM6PS7zoC8R1TGDVj5-SyOmhVN4y1Jo0KzsMtBg7oOdGaAJxGO_Q6FSJRNlimKRGjC_p_Nw4rfJrh9ak4oN1dtzyXV0e0GjTsso72MFHxGSew6AS4pfoxdayawx6fKcXzHsRpu56eCWb9vcSpPIEdc9fubsM6ZbOLLWVr65Yayr8SP6gotyQJ71oIQHT0bgtivvYS3YxYu8c1EjQ1yzgw3Hji_Jg'

The expected response should be 200 and you should find the scim2 users listed in the response body.

--

--