getDPoPHeader function
Generates a DPoP (Demonstrating Proof-of-Possession) proof JWT header for OAuth 2.0 requests.
Creates a signed JWT that proves possession of a private key and binds the request to a specific endpoint, method, and time.
The proof can be used for both token requests and protected resource access.
Parameters:
clientId
: The OAuth client identifierendpoint
: The complete URL of the endpoint being accessedmethod
: The HTTP method of the request (e.g., 'POST', 'GET')dPoPNonce
: The DPoP nonce provided by the serverauthorizationServer
: Optional. The authorization server URL for access token bindingaccessToken
: Optional. The access token to bind to this proofpublicKey
: The encoded EC P-256 public key in the format used by this libraryprivateKey
: The encoded EC P-256 private key for signing the proof
Returns a DPoP proof as a signed JWT string in the format: header.payload.signature
The generated proof includes: Header (JWK):
{
"alg": "ES256",
"typ": "dpop+jwt",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
Payload:
{
"sub": "client_id",
"htu": "endpoint_url",
"htm": "http_method",
"exp": timestamp + 60,
"jti": "random_unique_id",
"iat": timestamp,
"nonce": "dpop_nonce",
"iss": "client_id_or_auth_server",
"ath": "optional_access_token_hash"
}
The proof is signed using ES256 (ECDSA with P-256 and SHA-256).
Example:
final dpopHeader = getDPoPHeader(
clientId: 'client123',
endpoint: 'https://bsky.social/...',
method: 'POST',
dPoPNonce: 'server-provided-nonce',
publicKey: encodedPublicKey,
privateKey: encodedPrivateKey
);
The generated header should be included in HTTP requests using the 'DPoP' header:
headers: {
'DPoP': dpopHeader
}
Note: The proof has a short expiration time (60 seconds) to prevent replay attacks. A new proof should be generated for each request.
Implementation
String getDPoPHeader({
required String clientId,
required String endpoint,
required String method,
required String dPoPNonce,
String? authorizationServer,
String? accessToken,
required String publicKey,
required String privateKey,
}) {
final (x, y) = decodePublicKey(publicKey);
final header = <String, dynamic>{
'alg': 'ES256',
'typ': 'dpop+jwt',
'jwk': {
'kty': 'EC',
'crv': 'P-256',
'x': base64Url.encode(x).replaceAll('=', ''),
'y': base64Url.encode(y).replaceAll('=', ''),
}
};
final epoch = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
final payload = <String, dynamic>{
'sub': clientId,
'htu': endpoint,
'htm': method,
'exp': epoch + 60,
'jti': randomValue(),
'iat': epoch,
'nonce': dPoPNonce,
};
if (authorizationServer != null && accessToken != null) {
payload['iss'] = authorizationServer;
payload['ath'] = hashS256(accessToken);
} else {
payload['iss'] = clientId;
}
final headerBase64 =
base64UrlEncode(utf8.encode(jsonEncode(header))).replaceAll('=', '');
final payloadBase64 =
base64UrlEncode(utf8.encode(jsonEncode(payload))).replaceAll('=', '');
final jwtMessage = '$headerBase64.$payloadBase64';
final jwtSignature = base64Encode(_sign(privateKey, jwtMessage));
return '$headerBase64.$payloadBase64.$jwtSignature';
}