eSeGeCe
software
OAuth 2.0 access tokens are the keys to your API kingdom — and if someone steals one, they can use it from anywhere. DPoP (Demonstrating Proof of Possession), defined in RFC 9449, solves this by cryptographically binding tokens to the client that requested them. Even if a token is intercepted, it becomes useless without the client's private key.
Starting with sgcWebSockets 2026.4.0, the OAuth2 client and server components include full DPoP support. This article explains what DPoP is, how it works, and how to configure it in your Delphi application.
In standard OAuth 2.0, a Bearer token works like cash — whoever holds it can spend it. If an attacker intercepts the token via a compromised network, leaked logs, or a malicious proxy, they have full access to your protected resources.
DPoP changes this by making tokens work like a credit card with a PIN. The token itself is bound to a public key, and every request must include a proof JWT signed by the corresponding private key. The server verifies that the proof matches the bound key before accepting the request.
|
Bearer Token (traditional) Anyone with the token can use it. Stolen tokens are fully exploitable. No proof of identity required. |
DPoP Token (sender-constrained) Token is bound to a key pair. Every request requires a fresh signed proof. Stolen tokens are worthless without the private key. |
The DPoP flow adds a lightweight cryptographic step to the standard OAuth2 process:
1. Key Generation — The client generates an asymmetric key pair (ES256 or RS256). The private key stays on the client; the public key is included as a JWK in DPoP proofs.
2. Token Request — When requesting an access token, the client includes a DPoP HTTP header containing a signed JWT proof. The proof includes the HTTP method, URL, timestamp, and a unique identifier.
3. Token Binding — The authorization server verifies the proof signature, extracts the JWK thumbprint (SHA-256 hash of the public key), and binds it to the issued token. The response contains token_type: DPoP instead of Bearer.
4. Resource Access — For every API call, the client sends both the token and a fresh DPoP proof. The proof includes an ath claim (SHA-256 hash of the access token), binding the proof to that specific token.
A DPoP proof is a compact JWT with three parts: header, payload, and signature.
// Header - identifies the key and algorithm
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "cWs37kZLJMej6fpd...",
"y": "e2bkcQGaBERgSZUb..."
}
}
// Payload - proves freshness and binds to the request
{
"htm": "POST",
"htu": "https://auth.example.com/oauth/token",
"iat": 1774950263,
"jti": "F1AFCD1F-95F7-401B-A2F5-195A31DB1802",
"ath": "fUHyO2r2Z3DZ53EsNr..."
}
Use OpenSSL to generate an elliptic curve key pair:
# Generate EC private key
openssl ecparam -name prime256v1 -genkey -noout -out dpop_private.pem
# Extract public key parameters
openssl ec -in dpop_private.pem -text -noout
Convert the public key X and Y coordinates to Base64URL to build the JWK:
{"kty":"EC","crv":"P-256","x":"cWs37kZLJMej6fpdyKaI8Gz6CE...","y":"e2bkcQGaBERgSZUbAGR-iOOM..."}
// Configure OAuth2 as usual
OAuth2.OAuth2Options.GrantType := auth2CodePKCE;
OAuth2.OAuth2Options.ClientId := 'your-client-id';
OAuth2.OAuth2Options.ClientSecret := 'your-client-secret';
OAuth2.AuthorizationServerOptions.AuthURL := 'https://auth.example.com/authorize';
OAuth2.AuthorizationServerOptions.TokenURL := 'https://auth.example.com/oauth/token';
// Enable DPoP
OAuth2.DPoPOptions.Enabled := True;
OAuth2.DPoPOptions.Algorithm := dpopES256;
OAuth2.DPoPOptions.PrivateKey.LoadFromFile('dpop_private.pem');
OAuth2.DPoPOptions.PublicKeyJWK := '{"kty":"EC","crv":"P-256","x":"...","y":"..."}';
// Start the OAuth2 flow - DPoP headers are added automatically
OAuth2.Start;
That's it. The component automatically:
DPoP-Nonce server challenges and retries transparentlyAfter obtaining a DPoP-bound access token, generate a proof for each API request:
var
vProof: String;
begin
// Generate a DPoP proof for the API call
vProof := OAuth2.GetDPoPProof(
'GET',
'https://api.example.com/userinfo',
OAuth2.AccessToken
);
// Include both headers in your HTTP request:
// Authorization: DPoP <access_token>
// DPoP: <proof_jwt>
HTTPClient.Request.CustomHeaders.AddValue('Authorization',
'DPoP ' + OAuth2.AccessToken);
HTTPClient.Request.CustomHeaders.AddValue('DPoP', vProof);
HTTPClient.Get('https://api.example.com/userinfo');
end;
DPoP is already supported by major OAuth2 providers:
| Auth0 | Enable DPoP in application settings. Requires nonce support (handled automatically). |
| Okta | Configure DPoP in authorization server access policies. GA since 2024. |
| Microsoft Entra ID | Supports DPoP for confidential clients. |
| Ping Identity | Full DPoP support in PingOne and PingFederate. |
The sgcWebSockets OAuth2 server component also supports DPoP validation. When OAuth2Options.DPoP is enabled, the server automatically:
token_type: DPoP for DPoP-bound tokensinvalid_dpop_proof)ath claim (access token hash) on resource requests// Enable DPoP on the OAuth2 server
OAuth2Server.OAuth2Options.DPoP := True;
// Optional: custom validation via event
OAuth2Server.OnOAuth2ValidateDPoP := procedure(Sender: TObject;
Connection: TsgcWSConnection;
const DPoPProof, AccessToken: String;
var IsValid: Boolean)
begin
// Add custom checks here (e.g., verify against a key registry)
IsValid := True;
end;
| Property / Method | Description |
|---|---|
DPoPOptions.Enabled |
Enables DPoP for all token requests. |
DPoPOptions.Algorithm |
dpopES256 (recommended) or dpopRS256. |
DPoPOptions.PrivateKey |
PEM-encoded private key for signing DPoP proofs. |
DPoPOptions.PublicKeyJWK |
JSON Web Key representation of the public key. |
GetDPoPProof() |
Generates a DPoP proof JWT for a specific HTTP method, URL, and access token. |
GetDPoPJWKThumbprint() |
Returns the RFC 7638 SHA-256 thumbprint of the public key. |
DPoPNonce |
The current DPoP-Nonce value from the server (read-only). |
DPoP is the most significant improvement to OAuth 2.0 token security since PKCE. It eliminates the entire class of token theft attacks with minimal code changes — just set DPoPOptions.Enabled := True, provide your keys, and the sgcWebSockets component handles the rest.
DPoP support is available in sgcWebSockets 2026.4.0 for both Delphi (DXE6 through D13) and .NET (.NET Framework 2.0 through .NET 9). Download the latest version at esegece.com.
When you subscribe to the blog, we will send you an e-mail when there are new updates on the site so you wouldn't miss them.