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
Client generates a private+public key
Client generates a DPoP proof (JWT signed with the private key)
Client submits request to IdentityProviders token endpoint asking for an access token to a given resource server
Request contains DPoP header containing the DPoP proof
Request is authenticated with ClientID+ClientSecret or PKCE
Scope name(s) are also part of request, identifying which resource server and which part(s) of the resource server client wants to access
If authenticated, IdentityProvider returns an access token, by default valid for one hour
The access token for DPoP contains a claim 'cnf' (confirmation) which contains the thumbprint of the clients public key
Client generates a DPoP proof for the resource server call bound to the access token
Client calls resource server with access token and DPoP proof in a header value
Resource server authenticates access token
Validate token as normal (issued by IdentityProvider)
Resource servers supporting DPoP MUST ensure that the public key from the DPoP proof matches the one bound to the access token
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:
typ
(Type):This claim defines the type of the token, set to
dpop+jwt
.
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.
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"
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"
iat
(Issued At)This claim specifies when the DPoP Proof was created
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.
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
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
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
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:
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.
One of the values of aud in the token is equal to the one set for the API
The scope value(s) match authorization in your API-endpoint
The value of iss in the token is equal to e.g. (producition environment): https://connect.visma.com
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"
}