Sample code for 30+ languages & platforms
Perl Requires Chilkat v11.4.0+

Implementing DPoP with OAuth2 Client Credentials

See more AI Examples

Demonstrating Proof-of-Possession (DPoP) is an OAuth 2.0 extension (RFC 9449) that secures access tokens by binding them to a specific client’s private key, preventing stolen token misuse. It requires the client to sign a JWT (DPoP Proof) with every request, proving they hold the private key.

This example shows how to automatically add the following headers to HTTP requests:

Authorization: DPoP <access_token>
DPoP: <new JWT signed with same key>

Chilkat Perl Downloads

Chilkat2 Perl
use strict;
use warnings;
use Chilkat2;

my $privKey = Chilkat2::PrivateKey->new;

# Load your ECDSA private key.  (Keys uses with DPoP must always be ECDSA keys.  Not RSA.)
my $success = $privKey->LoadAnyFormatFile('qa_data/pem/ecc_privKey.pem', '');
if (!$success) {
    print $privKey->LastErrorText, "\n";
    exit;
}

my $tokenEndpoint = 'https://demo.duendesoftware.com/connect/token';
my $apiEndpoint = 'https://demo.duendesoftware.com/api/dpop/test';

# Provide the details needed for Chilkat to automatically fetch the access token
# using the client_credentials OAuth2 flow.
my $json = Chilkat2::JsonObject->new;
$json->UpdateString('client_secret', 'secret');
$json->UpdateString('client_id', 'm2m.dpop');
$json->UpdateString('token_endpoint', $tokenEndpoint);
$json->UpdateString('scope', 'api');

my $http = Chilkat2::Http->new;
$http->SessionLogFilename('c:/aaworkarea/sessionLog.txt');

# Provide private key to be used with DPoP
$success = $http->SetDPoPKey($privKey);
if (!$success) {
    print $http->LastErrorText, "\n";
    exit;
}

$http->AuthToken($json->Emit());

# When we make the first request to an API endpoint, we don't yet have
# an OAuth2 access token.  Chilkat (internally) will use the token endpoint
# to automatically get an access token.
# Chilkat will then add the following headers to the request sent to the API endpoint:

# Authorization: DPoP <access_token>
# DPoP: <new JWT signed with same key>

# For subsequent requests using the same Chilkat HTTP object instance,
# the same access token will be used with a newly generated DPoP header for each request.
# When the access_token is expired or about to expire, Chilkat will automatically (internally)
# refresh the access token.  Your application does not need to be concerned with the
# complexities of fetching the access token or managing the refresh, or generating
# the DPoP header for each request.

my $resp = $http->QuickGetStr($apiEndpoint);
if (!$http->LastMethodSuccess) {
    print $http->LastErrorText, "\n";
    exit;
}

my $statusCode = $http->LastStatus;

print 'response status code = ', $statusCode, "\n";
print $resp, "\n";

# Here is a Session log showing the raw HTTP request(s) and response(s).
# Notice how Chilkat automatically fetched the access_token and then used it in the API request.

# ---- Sending Mon, 30 Mar 2026 20:09:44 GMT ----
# POST /connect/token HTTP/1.1
# Host: demo.duendesoftware.com
# Content-Type: application/x-www-form-urlencoded
# Content-Length: 79
# DPoP: ****
#
# client_secret=secret&client_id=m2m.dpop&scope=api&grant_type=client_credentials
# ---- Received Mon, 30 Mar 2026 20:09:44 GMT ----
# HTTP/1.1 200 OK
# Date: Mon, 30 Mar 2026 20:09:43 GMT
# Content-Type: application/json; charset=UTF-8
# Transfer-Encoding: chunked
# Connection: keep-alive
# Server: Kestrel
# Cache-Control: no-store, no-cache, max-age=0
# Pragma: no-cache
# X-REVISION: 4f42f7e7
#
# 374
# {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjE1OEUyRDQ1NzhFNDMyOUJBMzRGNzkxQTQ4Nzk5NzYwIiwieDV0IjoiczkyM2RnYnhWbnhoaTJSZmR1cWZyZXZIYUUwIi
# widHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2RlbW8uZHVlbmRlc29mdHdhcmUuY29tIiwibmJmIjoxNzc0OTAxMzgzLCJpYXQiOjE3NzQ5MDEzODMsImV4cCI6MTc3NDkwN
# Dk4MywiYXVkIjoiYXBpIiwiY25mIjp7ImprdCI6IlVMR2J4ZXdNOEZUNDhTd3hKeUY3YnJuMDZWMDdPZ3VpdW41d1ZzSVVENEUifSwic2NvcGUiOlsiYXBpIl0sImNsaWVudF9pZCI6
# Im0ybS5kcG9wIiwianRpIjoiQ0Q5MjBFQzQyOTRBOTA3QUUyRjMyQjlCREQzQTA3QzMifQ.bbq8tNMVtJp79Wc9xrAsE0h8oOfdxEKtGZ3qHpJX-mvSS4FVXLoO2bfpq1nhFryfbZkk
# ENfoIwSQjb_35aziq2NrnpWwQ4vDF1b9K-b7eJXPmJybm61DAP9HSE3ZI0jMrMTRFLtjwvE4WV6ZXNTwN0Pzn6teog1i09PZQkaGtlJEBjsBaN5N7OsQvtgFLD0sNkjy1JTw5LMNE7u
# 5-W32qv-XDk9KEGfoBkqyt-SR_A2wI7oZvl4OGnYyX8tUOFszlHZKy0_0V3Oc9wkHRM2JZDA_LfTqldJHR5CF5FuNiJ2hp-kioUA-i0gWxhqnVKVspJqQAN3Z95RDl9jSf-pYSg",
# "expires_in":3600,"token_type":"DPoP","scope":"api"}
# 0
#
#
# ---- Sending Mon, 30 Mar 2026 20:09:44 GMT ----
# GET /api/dpop/test HTTP/1.1
# Host: demo.duendesoftware.com
# Accept: */*
# Accept-Encoding: gzip
# Authorization: ****
# DPoP: ****
#
#
# ---- Received Mon, 30 Mar 2026 20:09:44 GMT ----
# HTTP/1.1 200 OK
# Date: Mon, 30 Mar 2026 20:09:44 GMT
# Content-Type: application/json; charset=utf-8
# Transfer-Encoding: chunked
# Connection: keep-alive
# Server: Kestrel
# X-REVISION: 4f42f7e7
#
# 420
# [{"type":"iss","value":"https://demo.duendesoftware.com"},{"type":"nbf","value":"1774901383"},{"type":"iat","value":"1774901383"},
# {"type":"exp","value":"1774904983"},{"type":"aud","value":"api"},{"type":"cnf","value":"{\u0022jkt\u0022:\u0022ULGbxewM8FT48SwxJyF7brn06V07Oguiun5wVsIUD4E\u0022}"},
# {"type":"scope","value":"api"},{"type":"client_id","value":"m2m.dpop"},{"type":"jti","value":"CD920EC4294A907AE2F32B9BDD3A07C3"},
# {"type":"authorization_scheme","value":"DPoP"},{"type":"proof_token","value":"eyJhbGciOiJFU .... aGrGOgkRXLQ"}]
# 0
#
#