Securing OAuth2 Tokens using DPoP with WSO2 Identity Server
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.
- 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
- 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).
- 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. - Open the deployment.toml in the
<IS_HOME>/repository/conf
directory, 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.
- 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.
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.
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.
Additional resources:
- https://github.com/wso2-extensions/identity-oauth-addons/tree/master/component/org.wso2.carbon.identity.dpop#readme
- https://darutk.medium.com/illustrated-dpop-oauth-access-token-security-enhancement-801680d761ff
- https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop-09
- https://htamahc.medium.com/oauth2-proof-of-possession-for-five-year-olds-b4baa04109b8
Thanks for reading!