Instructions for

On this page

Configure OAuth 2.0 Demonstrating Proof-of-Possession

This guide discusses how to create sender-constrained access tokens that are an app-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

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

) can then verify that binding, which provides assurance that the client presenting the token also possesses the private key.

OAuth 2.0 DPoP JWT flow

Before you configure DPoP

Create a DPoP proof JWT (opens new window). A DPoP proof JWT includes a header and payload with claims. Then, sign the JWT with the private key from your JSON Web Key (JWK). Use the DPoP proof JWT to obtain a DPoP-bound access token. To create a DPoP proof JWT, 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.

DPoP proof 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 be dpop+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 be none or an identifier for a symmetric algorithm. This example uses RS256.
  • jwk: JSON Web Key. Include the public key (in JWK string format). 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.

Example JWT header

  {
    "typ": "dpop+jwt",
    "alg": "RS256",
    "jwk": {
      "kty": "RSA",
      "e": "AQAB",
      "use": "sig",
      "kid": "XUl71vpgPXgxSTCYHbvbEHDrtj-adpVcxXH3TKjKe7w",
      "alg": "RS256",
      "n": "4LuWNeMa7.....zLvDWaJsF0"
    }
  }

Include the following required claims in the JWT payload:

Configure DPoP

This section discusses the initial POST /token request that you need to make, the JWT payload update, and the second POST /token request that includes the updated JWT.

  1. Make the initial request. Include the additional DPoP header (--header 'DPoP: eyJ0eXAiOiJkcG9w.....H8-u9gaK2-oIj8ipg') in your /token request. The value for the DPOP header is the DPoP proof JWT from the Before you configure DPoP section.

    The

    authorization server verifies the JWT in the request and sends back an "Authorization server requires nonce in DPoP proof" error. The dpop-nonce header and value are included in the headers of that response. The authorization server provides the 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.

    Example response

    HTTP/ 1.1 400 Bad Request
    Cache-Control: no-cache, no-store
    Pragma: no-cache
    Content-Type: application/json
    Server: nginx
    Date: Tue, 07 Mar 2023 23:43:13 GMT
    dpop-nonce: 8NLZUUhVawx1ns8AjrC4F6j8D2phvaw7
    
      {
        "error": "use_dpop_nonce",
        "error_description": "Authorization server requires nonce in DPoP proof."
      }
    
  2. Update the JWT payload.

    • Add the dpop-nonce header value from the response as the nonce claim value.
    • Include a jit claim, which is a unique JWT identifier (opens new window) for the request.

    Example payload:

  3. Copy the new DPoP proof and add it to the DPoP header in the second POST /token request for an access token. The

    authorization server should return the access token.

    Example response

    Note: Tokens are truncated for brevity.

      {
          "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:

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 DPoP-protected resource

Now that you have a DPoP-bound access token, you can make requests to DPoP-protected resources.

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.

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

Example response

{
    "token_type": "DPoP",
    "expires_in": 3600,
    "access_token": "eyJraWQiOiJRVXlGdjB.....RxDhLJievVVN5WQrAZlw",
    "scope": "offline_access openid",
    "refresh_token": "3CEz0Zvjs0eG9mu4w36n-c2g6YIqRfyRSsJzFAqEyzw",
    "id_token": "eyJraWQiOiJRVX.....3SA6LTm7mA"
}