On this page
Configure OAuth 2.0 Demonstrating Proof-of-Possession
This guide discusses how to create sender-constrained access tokens that are an application-level mechanism for preventing token replays at different endpoints.
Learning outcomes
- Understand the purpose of Demonstrating Proof-of-Possession
- Understand how to configure OAuth 2.0 Demonstrating Proof-of-Possession (DPoP) for your org and app
What you need
Overview
OAuth 2.0 Demonstrating Proof-of-Possession (DPoP) helps prevent unauthorized parties from using leaked or stolen access tokens. When you use DPoP, you create an application-level mechanism to sender-constrain both access and refresh tokens. This helps prevent token replays at different endpoints.
Note: The Okta DPoP feature is based on the current RFC (opens new window).
DPoP enables a client to prove possession of a public/private key pair by including a DPoP header in a /token
endpoint request. The value of the DPoP header is a JSON Web Token (JWT) and is called a DPoP proof. This DPoP proof enables the authorization server to bind issued tokens to the public part of a client's key pair. Recipients of these tokens (such as an
OAuth 2.0 DPoP JWT flow
Configure DPoP
This section explains how to configure DPoP in your org, and then how to create a DPoP proof (JWT) to obtain a DPoP-bound access token. A JWT is a compact, URL-safe way to represent claims transferred between two parties. A common use case example for JWTs is to declare the scope of the access token.
Configure the app integration
Create or update an app to include the DPoP parameter.
Create an app
- Sign in to your Okta organization with your administrator account and go to Applications > Applications.
- Click Create App Integration.
- Select OIDC - OpenID Connect, and then Native Application.
- Name your application and scroll down to the bottom of the page and select Allow everyone in your organization to access.
- Click Save and then click Edit in the General Settings section of the page that appears.
- Select the Require Demonstrating Proof of Possession (DPoP) header in token requests checkbox for Proof of possession.
- Click Save.
Use the API
You can also use the Apps API to create or update an OAuth 2.0 client app and enable the DPoP parameter. Use the following parameters in the request:
response_types
: This example uses the Authorization Code grant type, socode
is the correct response type.grant_types
: This example usesauthorization_code
andrefresh_token
.dpop_bound_access_tokens
: This example usestrue
to indicate that the app accepts DPoP-bound access tokens.
Note: See the Request Body Schema (opens new window) section of the Applications API reference for more information on the new DPoP parameter.
In the POST (create the client app) or PUT (update the client app) request, add the DPoP parameter to settings.oauthClient
:
{
"id":"0oafj3uhoKh5M9izF0g4",
"name":"oidc_client",
"label":"[Dev App] SPA Client",
"status":"ACTIVE",
"settings": {
"oauthClient": {
"response_types":["code"],
"grant_types":[
"authorization_code",
"refresh_token"
],
"application_type":"browser",
"wildcard_redirect":"DISABLED"
"dpop_bound_access_tokens": true
}
}
}
Create a JSON Web Key
Create a JSON Web Key (opens new window) (JWK) for use with DPoP. A JWK is a cryptographic key or key pair expressed in JSON format. You use the generated public and private keys to sign the JSON Web Token (JWT) for use with DPoP in the next section.
Note: The JWK that's used for DPoP authentication is separate from the JWK used for client authentication.
Use your internal instance of a key pair generator to generate the public/private key pair for use with DPoP in a production org. See this key pair generator (opens new window) for an example.
Note: Use only asymmetric keys with DPoP. See Asymmetric Encryption: Definition, Architecture, Usage (opens new window).
For testing purposes only, you can use this simple JWK generator (opens new window) to generate a key pair. Follow these steps if you use the simple JWK generator:
Select the following and then click Generate.
- Key Use: Signature
- Algorithm: RS256
- Key ID: SHA-256
- Show X.509: Yes
Copy the Public Key, the Private Key (X.509 PEM Format), and the Public Key (X.509 PEM Format) for use in the next steps.
Create the JSON Web Token
Create a DPoP proof JWT (opens new window). A DPoP proof JWT includes a header and payload with claims, and then you sign the JWT with the private key from the previous section.
Use your internal instance to sign the JWT for a production org. See this JWT generator (opens new window) for an example of how to make and use JWTs in Node.js apps. For testing purposes only, you can use this JWT tool (opens new window) to build, sign, and decode JWTs. See Use the JWT tool.
Parameters and claims
Include the following required parameters in the JWT header:
typ
: Type header. Declares that the encoded object is a JWT and meant for use with DPoP. This must bedpop+jwt
.alg
: Algorithm. Indicates that the asymmetric algorithm is RS256 (RSA using SHA256). This algorithm uses a private key to sign the JWT and a public key to verify the signature. Must not benone
or an identifier for a symmetric algorithm. This example usesRS256
.jwk
: JSON Web Key. Include the public key (in JWK string format) that you create in the Create a JSON Web Key section. Okta uses this public key to verify the JWT signature. See the Application JSON Web Key Response properties (opens new window) for a description of the public key properties.
Include the following required claims in the JWT payload:
Use the JWT tool
Follow these steps if you use the JWT tool (opens new window). See the previous section for parameter and claim descriptions.
- Select RS256 as the Algorithm.
- Build the JWT header in the HEADER section. Include the public key from the public/private key pair generated in the previous section.
{
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "XUl71vpgPXgxSTCYHbvbEHDrtj-adpVcxXH3TKjKe7w",
"alg": "RS256",
"n": "4LuWNeMa7.....zLvDWaJsF0"
}
}
- In the PAYLOAD section, build the JWT payload and include the following claims:
{
"htm": "POST",
"htu": "http://{yourOktaDomain}/oauth2/default/v1/token",
"iat": 1516239022
}
In the VERIFY SIGNATURE section:
- First box: Paste the public key (X.509 PEM format) from the previous section.
- Second box: Paste the private key (X.509 PEM format).
Copy the JWT that appears in the Encoded section.
Build the request
Your next step is to build the POST request to the /token
endpoint for an access token. Two requests to the /token
endpoint are necessary. The initial request obtains the dpop-nonce
header value from the
Okta custom
dpop-nonce
header value in the JWT payload. After you receive a nonce
value from the /token
endpoint, you can continue to use that value utnil you receive an error with a new dpop-nonce
header. The additional header in the initial request is DPoP
. The value for DPoP
is the DPoP proof JWT from the previous section.
Request example:
Note: Some values are truncated for brevity.
curl --request POST
--url 'https://{yourOktaDomain}/oauth2/default/v1/token' \
--header 'Accept: application/json' \
--header 'DPoP: eyJ0eXAiOiJkcG9w.....H8-u9gaK2-oIj8ipg' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code' \
--data 'redirect_uri=https://{yourOktaDomain}/app/oauth2' \
--data 'code=XGa_U6toXP0Rvc.....SnHO6bxX0ikK1ss-nA' \
--data 'code_verifier=k9raCwW87d_wYC.....zwTkqPqksT6E_s' \
--data 'client_id={clientId}'
The
Okta custom
dpop-nonce
header and value. The
Okta custom
dpop-nonce
value to limit the lifetime of DPoP proof JWTs and renews the value every 24 hours. The old dpop-nonce
value continues to work for three days after generation. Be sure to save the dpop-nonce
value from the token response header and refresh it every 24 hours. Use the value of the dpop-nonce
header in the JWT payload and update the JWT:
Add the
dpop-nonce
header value as thenonce
claim value in the JWT payload along with ajti
claim.Example payload:
{ "htm": "POST", "htu": "https://{yourOktaDomain}/oauth2/default/v1/token", "iat": 1516239022, "nonce": "dsGuZVkXzEdbNb8yxI3Fi-cnuzkH_E0k", "jti": "123456788" }
Claims
nonce
: Used only once. A recentnonce
value provided by the authorization server using thedpop-nonce
HTTP header. The authorization server provides the DPoP nonce value to limit the lifetime of DPoP proof JWTs.jti
: JWT ID. A unique JWT identifier (opens new window) for the request
Copy the new DPoP proof and add it to the DPoP header in the request.
Send the request for an access token again. The
Okta custom
{ "token_type": "DPoP", "expires_in": 3600, "access_token": "eyJraWQiOiJRVX.....wt7oSakPDUg", "scope": "openid offline_access", "refresh_token": "3CEz0Zvjs0eG9mu4w36n-c2g6YIqRfyRSsJzFAqEyzw", "id_token": "eyJraWQiOiJRVXlG.....m5h5-NAtVFdwD1bg2JprEJQ" }
Decode the access token
You can use the JWT tool (opens new window) to decode the access token to view the included claims. The decoded access token should look something like this:
{
"ver": 1,
"jti": "AT.pKoLFoM7X4P4DrJBRvXaJzj9g0-naK1ChGH_oTbStYE",
"iss": "https://{yourOktaDomain}/oauth2/default",
"aud": "api://default",
"iat": 1677530933,
"exp": 1677534533,
"cnf": {
"jkt": "2HR2BW5-tan1aI6yIPHVOHwirAy4kQGWULoQHKUO0s4"
},
"cid": "0oa4dr9kzkykPrLhq0g7",
"uid": "00u47ijy7sRLaeSdC0g7",
"scp": [
"openid"
],
"auth_time": 1677521913,
"sub": "user@example.com"
}
Claims
cnf
: Confirmation. Claim that contains the confirmation method.jkt
: JWK confirmation method. A base64url encoding of the JWK SHA-256 hash of the DPoP public key (in JWK format) to which the access token is bound.
Note: If your client has DPoP enabled, then you can't add or modify the
cnf
claim using token inline hooks.
Make a request to a non-Okta resource
Now that you have a DPoP-bound access token, you can make requests to DPoP-protected resources. The following example request displays the DPoP-bound access token in the Authorization
header and the DPoP proof JWT in the DPoP
header. Values are truncated for brevity.
curl -v -X GET \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: DPoP eyJraWQiOiJRVX.....wt7oSakPDUg' \
--header 'DPoP: eyJ0eXAiOiJkcG9w.....H8-u9gaK2-oIj8ipg' \
"https://resource.example.org"
Validate token and DPoP header
The resource server must perform validation on the access token to complete the flow and grant access. When the client sends an access request with the access token, validation should verify that the cnf
claim is present. Then validation should compare the jkt
in the access token with the public key in the JWT value of the DPoP
header.
The following is a high-level overview of the validation steps that the resource server must perform.
Note: The resource server must not grant access to the resource unless all checks are successful.
- Read the value in the
DPoP
header and decode the DPoP JWT. - Get the
jwk
(public key) from the header portion of the DPoP JWT. - Verify the signature of the DPoP JWT using the public key and algorithm in the JWT header.
- Verify that the
htu
andhtm
claims are in the DPoP JWT payload and match with the current API request HTTP method and URL. - Calculate the
jkt
(SHA-256 thumbprint of the public key). - Extract the DPoP-bound access token from the
Authorization
header, verify it with Okta, and extract the claims. You can also use the/introspect
endpoint (opens new window) to extract the access token claims. - Validate the token binding by comparing
jkt
from the access token with the calculatedjkt
from theDPoP
header.
For instructional purposes, this guide provides example validation in a Node.js Express app using the third-party site Glitch. Glitch is a browser-based development environment that can build a full-stack web application online. Use the Glitch example to review and quickly implement the validation code. It includes all dependencies required to complete validation.
Copy (remix on Glitch) the Validation DPoP Tokens (opens new window) Glitch project to have a functional code sample. The validation steps at the beginning of this section are included in the code for quick implementation.
Note: See Libraries for Token Signing/Verification (opens new window) to view other libraries/SDKs in different languages that you can use for JWT verification.
Refresh an access token
To refresh your DPoP-bound access token, send a token request with a grant_type
of refresh_token
. Then, include the same DPoP
header value that you used to obtain the refresh token in the DPoP
header for this request. Include the openid
scope when you also want to refresh an ID token. In the following examples, tokens are truncated for brevity.
Example request
curl --request POST
--url 'https://{yourOktaDomain}/oauth2/default/v1/token' \
--header 'Accept: application/json' \
--header 'DPoP: eyJ0eXAiOiJkcG9w.....H8-u9gaK2-oIj8ipg' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=refresh_token' \
--data 'redirect_uri={redirectUri}' \
--data 'client_id={clientId}' \
--data 'scope=offline_access openid' \
--data 'refresh_token=3CEz0Zvjs0eG9mu4w36n-c2g6YIqRfyRSsJzFAqEyzw'
Example response
{
"token_type": "DPoP",
"expires_in": 3600,
"access_token": "eyJraWQiOiJRVXlGdjB.....RxDhLJievVVN5WQrAZlw",
"scope": "offline_access openid",
"refresh_token": "3CEz0Zvjs0eG9mu4w36n-c2g6YIqRfyRSsJzFAqEyzw",
"id_token": "eyJraWQiOiJRVX.....3SA6LTm7mA"
}