Consepts_Demonstrating Proof of Posession (DPoP) - FHIDev/Fhi.HelseId GitHub Wiki
What is DPoP
DPoP (Demonstrating Proof of Posession) is an addition to the OAuth specification (RFC9449) that mitigates the risk of an attacker obtaining an access token to access your APIs. It is a requirement in the NHN security profile.
TL;DR summary (what you must know)
- DPoP is a new type of access tokens that replaces Bearer tokens
- DPoP prevents theft of access tokens and replay attacks against your services
- Moving to DPoP tokens is a breaking change
- We will have to transition from Bearer to DPoP tokens
TL;DR - what you must do
- Upgrade to latest version of Fhi.HelseId.Web and Api to enable accepting DPoP tokens
- Upgrade to version 2.x of Fhi.ClientCredentials (see readme for how-to) and set "UseDpop" to true when the API accepts DPoP
- Configure DPoP behavior in appsettings, set "RequireDPoPTokens" to true when all your clients use DPoP tokens
How to use DPoP
To use DPoP, you must upgrade to the latest version of Fhi.HelseId. APIs will automatically start accepting DPoP tokens, but clients will have to be configured to use DPoP tokens. Client should use the Fhi.ClientCredentials package for integrating with applications. Since switching from Bearer to DPoP is a breaking change for clients, this can be controlled by setting UseDpop to true in appsettings:
"ClientCredentialsConfiguration": {
"Apis": [
{
"Name": "IGrunndataClient",
"Url": "https://localhost:5001",
"Scope": "fhi:grunndata.personoppslagapi/sysvak",
"UseDpop": true
}
],
"clientName": "...",
...
APIs
APIs will automatically accept DPoP tokens by default as long as the Fhi.HelseId packages are updated. However, to be compliant with the NHN security profile, the flag RequireDPoPTokens must be set to true to make the API only accept DPoP tokens. We have set this flag to false by default to ensure a smooth transition to DPoP tokens. When you know that all your clients are using DPoP instead of Bearer access tokens, the RequireDPoPTokens can be set to true. A warning will be logged for each application startup until this flag is set to true to warn about the security requirement of using DPoP tokens.
Configurable properties in appsettings:
"HelseIdApiKonfigurasjon": {
...
"RequireDPoPTokens": false,
"AllowDPoPTokens": true // true by default
}
Web
Web applications (that have a user login) can start using DPoP tokens by setting UseDPoPTokens to true in appsettings. This is not enabled by default as many web applications forward their tokens to underlying APIs.
"HelseIdWebKonfigurasjon": {
"UseDPoPTokens": true,
"Apis": [
...
]
},
Note about token forwarding
Token forwarding is not a pattern we would encourage as each application should have unique tokens between them to ensure proper access control. Some services are still forwarding tokens and we are therefore providing tools for making it easy to use DPoP in those scenarios. Web applications (with a user login) should forward tokens automatically as they are part of the built-in part of Fhi.HelseId.Web access token forwarding. If other mechanisms are built outside of this, those implementations must set a new DPoP proof value on the forwarded HTTP requests. See the class DPoPAuthorizationHeaderSetter for an example of how this can be done.
How does it work
In short, DPoP authentication tokens have an associated proof. The proof will change on each request and is signed using a private key by the client. The public key is attached to the proof and is used to verify that the proof is valid. An API will not accept the same proof more than one time.
Compared with Bearer authentication tokens, DPoP has its own authentication scheme (DPoP) in addition to a DPoP-header (the proof) which is sent alongside the HTTP request.
Normally your HTTP request has a Bearer authentication token, like this:
POST https://test-personoppslagapi-grunndata.fhi.no/V2/Person HTTP/1.1
Host: test-personoppslagapi-grunndata.fhi.no
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc4NjY3R...
Content-Type: application/json; charset=utf-8
Content-Length: 125
{"fnr":"03742297208","id":null,"key":null,"inclHistory":true,"infoParts":"AllFhi","constructParts":"Alle","inclRawdata":true}
With DPoP, the Authentication scheme is changed to DPoP and it also introduces a new DPoP header (referred to as the proof). Both of these are JWKs where the STS signs the access token JWK using its private key, while the client uses its private key to sign the DPoP proof.
POST https://test-personoppslagapi-grunndata.fhi.no/V2/Person HTTP/1.1
Host: test-personoppslagapi-grunndata.fhi.no
Authorization: DPoP eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc4NjY3Rjk...
DPoP: eyJhbGciOiJQUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7I...
Content-Type: application/json; charset=utf-8
Content-Length: 125
{"fnr":"03742297208","id":null,"key":null,"inclHistory":true,"infoParts":"AllFhi","constructParts":"Alle","inclRawdata":true}
Authorization header (access token)
The DPoP authentication header introduces a new jkt-claim, which is a thumbprint of the JWK in the DPoP proof. The jkt value binds the access token to the public key being used by the client. This is originally part of the token request to the STS (authorization server), and is therefore part of the access token and can be trusted as it is part of the signed JWK.
JWT Body (shortened for brevity, the rest is identical to a Bearer token):
{
...
"cnf": {
"jkt": "2qFx_g8VE9e2TGrekkhLQ-2W4ChWvEHUtpW7tlRvJk8"
}
...
}
Proof header
The proof is a JWT that consists of the following parts:
Header:
{
"alg": "PS256",
"typ": "dpop+jwt",
"jwk": {
"alg": "PS256",
"e": "AQAB",
"kty": "RSA",
"n": "whMxivhxWOhB3cPnyNU1eLzyRMazE0f9uEZiDtxfoOTbWgVVQcPLtGLhKuNQbc9KAQag4PSke52ftDWm59uajocI0iM3GhKEECc_f2kJSBZJwGsiSR2J_Rc1cjYybKzidIL5WTxT8vn60QvoBAp4E7YsG5lpLP8fQB3FO2F1NwZmtxELzUBkpgreXEjSzuYZ98KbQBDAp-HjyunKDjXRg_ih7W4fm5WQaPNxjjDrpTHcgTMHp4GYfdQE4VcRAA4QNtj5XZJ0FXMWKIJmI5fokBkrGO7FXBuFUS55EgDpZNA8z-r0FrhXxA-vNJ8TRtR5MbjuEn15XDNRVHtigy3boaSpXInnNsuaJVmiKmfJeVNvTEqJazOuI3T97IsVNoVwptTdViy4GVpVLobMdVyeKC5GWGkvOGhWi2_aRlJSO4TJogs6iKUp3wKQFAKdSBZeBG79tT5neX9lL2QkqdPKNKt0IKwpQ-5Tkjtle84ke-dcb7XqER2DxWp_oky8w8MmkfBE1JpL2M_bl7N2W4QFt46q30awqGqdX97j4PbtR1hNB4mIjLYRuls_1xgf-h9gIqrCzyuKdBzWEEpWHpULHf4TDrS9CG2rnPaeoUnoUB4DJS_lqZmeGWz1dCnyKcCaPu4In-2XyW_gUSYP5rA0nhJfgcwIwooiuVaQZ8-WSSE"
}
}
The important part about the header is the jwk value which contains the public key that is used to verify that this is a legigimate proof together with the signature of the JWT (proof) itself. Note that the thumbprint of this has to match the jtk value in the access token.
Body:
{
"jti": "206589d4-cc29-46e5-97bd-fe087b6cd5c6",
"htm": "POST",
"htu": "https://localhost:5001/V2/Person",
"iat": 1727682360,
"ath": "AvuXYAh488H6WHppQ37CRVg3g6ZjL2ys1n6PRZig3wI"
}
The body contains a unique value (jti), which HTTP method is being used (htm), the URL (htu), a timestamp (iat), and a hash of the access token (ath). The hash ties the proof to the access token.
With DPoP, a client will have public/private key-pair from the HelseID configuration, where the private key is used to sign JWTs. This JWT will also include a timestamp, a unique identifier, the URL of the API endpoint, and the HTTP method.