getDPoPHeader function

String getDPoPHeader({
  1. required String clientId,
  2. required String endpoint,
  3. required String method,
  4. required String dPoPNonce,
  5. String? authorizationServer,
  6. String? accessToken,
  7. required String publicKey,
  8. required String privateKey,
})

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 identifier
  • endpoint: The complete URL of the endpoint being accessed
  • method: The HTTP method of the request (e.g., 'POST', 'GET')
  • dPoPNonce: The DPoP nonce provided by the server
  • authorizationServer: Optional. The authorization server URL for access token binding
  • accessToken: Optional. The access token to bind to this proof
  • publicKey: The encoded EC P-256 public key in the format used by this library
  • privateKey: 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';
}