DPoP (Proof of Posession)

Prev Next

The DPoP (Demonstrating Proof of Possession) standard is a mechanism for enhancing the security of OAuth 2.0 access tokens.

It works by cryptographically binding an access token to the specific client (application) it was issued to. This binding ensures that only the legitimate client can use the token, even if it's intercepted by a third party.

This kind of authorization is specified in RFC 9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP).

DPoP in Visma Connect IdP

Visma Connect IdP supports DPoP both for access tokens and refresh tokens. It is available for both service (non-interactive) and user (interactive) APIs and applications.

In Developer Portal it is possible to enable it for an API, by enforcing DPoP for all integrators requesting token for the API. It is also possible to combine with Bearer tokens to support both token types.

For applications in Developer Portal, DPoP can also be enforced. DPoP is then required when the application requests tokens from Connect.

Getting Started

  • Client (application which want to call an API)

  • Resource server (the API the client wants to call)

  • IdentityProvider (authorization server issuing tokens)

    Basic Flow

  1. Client generates a private+public key

  2. Client generates a DPoP proof (JWT signed with the private key)

  3. Client submits request to IdentityProviders token endpoint asking for an access token to a given resource server

    1. Request contains DPoP header containing the DPoP proof

    2. Request is authenticated with ClientID+ClientSecret or PKCE

    3. Scope name(s) are also part of request, identifying which resource server and which part(s) of the resource server client wants to access

  4. If authenticated, IdentityProvider returns an access token, by default valid for one hour

    1. The access token for DPoP contains a claim 'cnf' (confirmation) which contains the thumbprint of the clients public key

  5. Client generates a DPoP proof for the resource server call bound to the access token

  6. Client calls resource server with access token and DPoP proof in a header value

  7. Resource server authenticates access token

    1. Validate token as normal (issued by IdentityProvider)

    2. Resource servers supporting DPoP MUST ensure that the public key from the DPoP proof matches the one bound to the access token

  8. If authenticated, resource server will process the request and return a response to client

Sequence diagram

DPoP Proof explained

A DPoP Proof is a JSON Web Token (JWT). It contains a set of claims that carry specific information. Here's a breakdown of the key claims:

  1. typ (Type):

    • This claim defines the type of the token, set to dpop+jwt.

  2. jti (JWT ID):

    • This claim is a unique identifier for the JWT.

    • It helps prevent replay attacks by ensuring that each DPoP Proof is used only once.

  3. htu (HTTP URI):

    • This claim represents the HTTP URI of the protected resource being accessed.

    • It is crucial for binding the proof to a specific endpoint.

    • The server verifies that the htu matches the target URI of the request.

    • Example: "htu":"https://example.com/resource"

  4. htm (HTTP Method):

    • This claim specifies the HTTP method used in the request (e.g., "GET," "POST," "PUT," "DELETE").

    • It further binds the proof to a specific action on the resource.

    • The server checks that the htm matches the request's method.

    • Example: "htm":"POST"

  5. iat (Issued At)

    • This claim specifies when the DPoP Proof was created

  6. Public Key (Embedded in the JWT Header):

    • While not a "claim" in the JWT payload (the claims section), the public key used to sign the DPoP Proof is essential. It is included in the JWT header, often using the jwk (JSON Web Key) parameter.

    • The server uses this public key to verify the signature of the DPoP Proof.

    • This is the critical element that binds the proof to the client's private key.

    • Supported signing algorithms are listed in Visma Connect well-known endpoint

Example requesting access token from Visma Connect with DPoP

Requesting access token from Visma Connect

To request an access token bound to a public key the client must provide a DPoP proof JWT in the DPoP header when making a token request to the authorization server.

This is applicable both when requesting access and refresh tokens.

  1. Generating DPoP Proof JWT

The DPoP proof JWT contains a header and a body and is signed using the clients private key.

Read more about signing and the JWT contents in the RFC: https://datatracker.ietf.org/doc/html/rfc9449#section-4.2

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

JWT example of a DPoP proof token:

eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2IiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiY2QyMDcxOTYtNjE1OC00YjYyLWEyNDEtYjM1ODVjNjA5Mjc5IiwiYWxnIjoiUlMyNTYiLCJuIjoibkszcTVwWFhQMnhGRU50d04wczZwZ0RCOTQ2NUYzWlVNWDI2dVltX0VfYmNBTGdLUEo1d29uZFdPQ0k4dXB6eVhPWW5iT3NXRDIxWExMcmpyUDF4YjFtQzQ3ZG56b01YVGhuM0VaVUlRb19vTGhuSkFxbHFwc05UZVdJOGtWSWdaNXFWbEh3U1VFSE14SEo3MVVYdzhRVGY3bXhzLWlGSnZCMHVwMXNFWlBvakZEdEVoU0VBQWFjdktMV0Q4NFBtRWdyN1J5cU0taTMzMHpWS0hvU0hVSWp0WlM2OTVNaEFRcWxqWktMVHlzTU1LNThNM2twVEdVRGpIeENIUkJvNHU5cGhZZ1JCbUtSR04yNTRZQ3RiRHI3M3lRcmQwUUd5dF9ZNFhVWG8zZjBtWWt5Z1lkeXZDZldVTlZ3Xzh2dlhMd3NlbzRGRzkyQ2FsSzRtTWVSLWZRIn19.eyJqdGkiOiJlYjhlOWQzMC02NGI3LTQ2YmQtOGI4Mi0wYjVmMzg0ODE1OGQiLCJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9jb25uZWN0LmlkZW50aXR5LnN0YWdhd3MudmlzbWEuY29tL2Nvbm5lY3QvdG9rZW4iLCJpYXQiOjE3MTA0OTU4MTN9.TQA_tAPZvZbghdFB4oBlCipe9uBlM1mR-_8ytl2Wtfo0KcUvrt-yjUzpciYtwkgG9pwuo05WjFnOrnwwsEdF01qWzN-1Io_mLec9OJ-lf0RI_koxsK6Ef5qOpyeaJVfgS7IJD8M2mPZeqxSx_m5KKF3w9r2pgNzHb0xiCi6A3ofGrSo-iUR90b8VI059z58WS6d1tIVtIDE42BRryIJ6rML5bivNcOrxKo04uliOkWFZaWW1FaxBoUFQV2_bjuseEymV2D9Lcy3as6gooyS-jzamMtcPmOMKBS6Pk-2uMw26dUZywGRnD-rzNZWhas-HtBbh7_vimjSanlvnCBYvPg

  1. Request token

    The DPoP proof JWT is included in a header (DPoP) when making the token request.

curl --request POST
  --url https://connect.visma.com/connect/token
  --header 'content-type: application/x-www-form-urlencoded'
  --header 'authorization: Basic base64encode("api_url:secret")'
  --header 'dpop: eyJ0eXAi.......mjSanlvnCBYvPg'
  --data 'token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjVENDc....7MTOBbdd5mgb2CHzxL0RFjs24pqC1pCeUqOjbg'

Parameters

Note: The parameters must be in the payload (body) of the request. Not as querystring parameters.

client_id

vismanet

yes *

client_id registered for your Application in Visma Developer Portal

client_secret

xyz123456xyz123456xyz123456xyz123456

yes *

client_secret issued by Visma Developer Portal


grant_type

client_credentials

yes

Specifies the type of grant being requested by the application. Client_credentials means client is sending client identifier + secret

scope

expense:reader expense:writer sms:client

no

Specifies the Application scope(s) your client has access to separated by a <space>. If not specified, a token with all your applications granted scopes will be issued

tenant_id

9c6c4ae0-6df2-4ea7-b5cf-5a6fc45dc3bf

no

tenant_id issued by Visma Connect Public API (Visma Connect Public API#Addatenant). Only relevant when dealing with APIs that relates to the Tenant ID

* = client_id and client_secret can be sent Authorization header (base64 encoded) instead of in request body


Response

All responses are of Content-Type: application/json

Success (200 OK)

{
  "access_token": "<tokenstring>",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Access token for DPoP authentication

For a DPoP-bound access token, the hash of the public key to which the token is bound is conveyed to the protected resource as metainformation in a token response. The hash is in the cnf (confirmation) claim value.

Example token:

{
  "iss": "https://connect.visma.com",
....
  "cnf": {
    "jkt": "arWOdBPJX2XaDgXofeorQsHjlWXva_DyWZqAY0al79Y"
  }
....
}
 
Token:
eyJhbGciOiJSUzI1NiIsImtpZCI6IkJCNzE3MEVFN0ZBMDFFNzE3QThFRUMxMzIwMDRDMDYzRDFBOTc3QTBSUzI1NiIsIng1dCI6InUzRnc3bi1nSG5GNmp1d1RJQVRBWTlHcGQ2QSIsInR5cCI6ImF0K0pXVCJ9.eyJpc3MiOiJodHRwczovL2Nvbm5lY3QuaWRlbnRpdHkuc3RhZ2F3cy52aXNtYS5jb20iLCJuYmYiOjE3MTA5Mjc3MDEsImlhdCI6MTcxMDkyNzcwMSwiZXhwIjoxNzEwOTMxMzAxLCJhdWQiOiJodHRwczovL2dyYXBoLWFwaS5jb25uZWN0LmlkZW50aXR5LnN0YWdhd3MudmlzbWEuY29tIiwiY25mIjp7ImprdCI6ImFyV09kQlBKWDJYYURnWG9mZW9yUXNIamxXWHZhX0R5V1pxQVkwYWw3OVkifSwic2NvcGUiOlsib3BlbmlkIiwiZ3JhcGhhcGk6cmVhZCJdLCJhbXIiOlsicHdkIl0sImNsaWVudF9pZCI6ImNsaWVudF9kcG9wX3JlcXVpcmVkIiwic3ViIjoiM2ZkNDlhZmItZGRmYi00OTUyLThlOGUtNTFjYzE1YjlmYjkzIiwiYXV0aF90aW1lIjoxNzEwOTI3MzA2LCJpZHAiOiJWaXNtYSBDb25uZWN0IiwibGx0IjoxNzEwNDk1ODEyLCJjcmVhdGVkX2F0IjoxNjc4OTY3MDg0LCJhY3IiOiIyIiwic2lkIjoiNDFlYTNhYzktZjU2My1mMjk3LTljNWUtNTI0NDA5Y2RiMzAzIn0.XhzicwQv5tvC711UMtQQSvRFjb7Jso--0n7oHuSQ62rP80SrwNM9ocMPI4jABVU5-nRw5ixVqj7CyUyjoP1eHO1htYRjZ0ocs9yV0fiLOBeiR6xyQT5ngZgGekcZV4IXRb6ukYzPQwwSYMQLEnFIdtihyt3bIQCHjbqqqF-lrPPhA-K80CjaM99vF2uUxWuFWx1n86HRu2pFcHG0XnTdY4gzKGK049yxUPBZkI_G05qOLshNALnQxMM_R_ne5RyeT_rvRgryjxhbZnfv_51oECi5pLGa_9-6q0YeWCN-L5sVs7_jlK4zumKhfOy8gosoWvZiasKEYbPHyHzYwqRSNw

  1. Sending token when calling API

Before sending the access token to an API, the client have to generate a new DPoP Proof JWT, bound to the access token and to the URL and method of the API.

Note: the JWT contains the "ath" value which is the SHA256 hash value of the access token.

Read more about signing and JWT contents in the RFC: https://datatracker.ietf.org/doc/html/rfc9449#section-6.1

DPoP API Proof example:

{
  "typ":"dpop+jwt",
  "alg":"ES256",
  "jwk": {
    "kty":"EC",
    "x":"l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
    "y":"9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
    "crv":"P-256"
  }
}
.
{
  "jti":"e1j3V_bKic8-LAEB",
  "htm":"GET",
  "htu":"https://resource.example.org/protectedresource",
  "iat":1562262618,
  "ath":"fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
}

Example Request sent to API

Note: the Authorization header type is DPoP instead of Bearer

curl
  --request POST
  --url https://resource.example.org/protectedresource
  --header 'content-type: application/json'
  --header 'authorization: DPoP eyJ0eXAiO......JJLR09tA'
  --header 'dpop: eyJ0eXAi.......mjSanlvnCBYvPg'
  --data '{ \"some_data\": \"Hellow World!\" }'

Example DPoP token:

eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IlJTMjU2IiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiNGMyODYxZTgtMDg0ZS00Mzg2LTk4NWQtZTlmZmYzOWRhODYxIiwiYWxnIjoiUlMyNTYiLCJuIjoibkszcTVwWFhQMnhGRU50d04wczZwZ0RCOTQ2NUYzWlVNWDI2dVltX0VfYmNBTGdLUEo1d29uZFdPQ0k4dXB6eVhPWW5iT3NXRDIxWExMcmpyUDF4YjFtQzQ3ZG56b01YVGhuM0VaVUlRb19vTGhuSkFxbHFwc05UZVdJOGtWSWdaNXFWbEh3U1VFSE14SEo3MVVYdzhRVGY3bXhzLWlGSnZCMHVwMXNFWlBvakZEdEVoU0VBQWFjdktMV0Q4NFBtRWdyN1J5cU0taTMzMHpWS0hvU0hVSWp0WlM2OTVNaEFRcWxqWktMVHlzTU1LNThNM2twVEdVRGpIeENIUkJvNHU5cGhZZ1JCbUtSR04yNTRZQ3RiRHI3M3lRcmQwUUd5dF9ZNFhVWG8zZjBtWWt5Z1lkeXZDZldVTlZ3Xzh2dlhMd3NlbzRGRzkyQ2FsSzRtTWVSLWZRIn19.eyJqdGkiOiJiNWIzZWE0Yi1mMzY5LTQ2MmMtOTk1Ny0yZTMzZDY2MGMyNzUiLCJodG0iOiJHRVQiLCJodHUiOiJodHRwOi8vbG9jYWxob3N0L2F1dGgiLCJpYXQiOjE3MTA0OTU5MjksImF0aCI6ImplVnNEUFFyQ2c0L1ZRempwRWhVSXZlYmkvc1RtR3o1akJaVXVidVlnblU9In0.CJgpcZD1hfC902ZheVFwH-8llJqSg8Gb_e7ukqCt1mCvsPnn6xtKyzCjZYILTx8vRaPOgz-9dGWzUudl_ZSmNW4jOhYELkzSt0U9ewAsCV0rhqUO7A1UHiud4OvvNLBIOtn2iY-RZ1YNpd5D33Xb9LUDTls5_8BULPtPv_lUnH-VNknPo3RiHrLNftwAjr5M1O0R-52ErMhjAaGi-77s3NwnP_TLtegcHvLxGsi8iynvYPPNC_NCiGcsEuR6uY1SleNrmJywSjoKo8pRC8-cLDMffK6_AuEdHYIq4bnAg9LF4QrR2Yaglh9ESaFPjA275Yp155Ps0oARabJJLR09tA

  1. APIs must verify the integrity of the access token

When the API receives the access token it must be verified. To verify that the token is valid, ensure that the following criterias are satisfied:

  1. The access token is properly signed by Visma Connect. Use Visma Connects public keys to verify the token's signature. The token is signed with JSON Web Key (signature present in JWT). JSON Web Key from Visma Connect is available at /.well-known/openid-configuration/jwks relative to the base address, e.g.: https://connect.visma.com/.well-known/openid-configuration/jwks. Your API should cache these keys for better performance until it receives a JWT token with a key identifier that does not match the one it has on cache, in which case the API must make another request to the above endpoint to check if there are new keys available and if keys changed proceed to use the new ones automatically.

  2. One of the values of aud in the token is equal to the one set for the API

  3. The scope value(s) match authorization in your API-endpoint

  4. The value of iss in the token is equal to e.g. (producition environment): https://connect.visma.com

  5. The expiry time (exp) of the ID token has not passed

The DPoP proof JWT should also be validated by the API. It is described in the DPoP RFC: https://datatracker.ietf.org/doc/html/rfc9449#name-checking-dpop-proofs

If authorized, the web service will process request. If denied, a 401 or 403 response is returned.

Example header showing algorithm and token type:

{
"alg": "RS256",
"kid": "5D471A9D7A32C57731A79DACA3EFBC5094E3EB07",
"typ": "at+jwt",
"x5t": "XUcanXoyxXcxp52so--8UJTj6wc"
}

Example payload of JWT showing notbeforetime (time to process if scheduled), expiry, issuer, audience, client and scope(s).

A tenant_id is only returned as a JWT claim when client has requested the access_token to include it.

{
  "nbf": 1510307843,
  "exp": 1510311443,
  "iss": "https://connect.visma.com",
  "aud": [
    "https://someservice.visma.net/api/v1"
  ],
  "client_id": "vismanet",
  "scope": [
    "someservice:read"
  ],
  "tenant_id": "9c6c4ae0-6df2-4ea7-b5cf-5a6fc45dc3bf"
}