verify method

DpopResult verify(
  1. String? proof, {
  2. required String method,
  3. required String htu,
  4. String? accessToken,
  5. String? expectedJkt,
  6. Set<String>? nonces,
})

Checks proof for a request with method to htu (normalized: scheme and host in lower case, default port removed, no query).

accessToken requires a matching ath; expectedJkt requires the proof's key to be the bound one; nonces replaces the accepted nonces (the gateway accepts only the nonce of its hello or reauth_required).

Implementation

DpopResult verify(
  String? proof, {
  required String method,
  required String htu,
  String? accessToken,
  String? expectedJkt,
  Set<String>? nonces,
}) {
  if (proof == null || proof.isEmpty) {
    return const DpopRejected('invalid_dpop_proof', 'no proof');
  }
  final parts = proof.split('.');
  if (parts.length != 3) {
    return const DpopRejected('invalid_dpop_proof', 'not a compact JWS');
  }
  final JsonMap header;
  final JsonMap payload;
  final Uint8List signature;
  try {
    header = _json(parts[0]);
    payload = _json(parts[1]);
    signature = _b64(parts[2]);
  } on FormatException {
    return const DpopRejected('invalid_dpop_proof', 'not decodable');
  }
  if (header['typ'] != 'dpop+jwt') {
    return const DpopRejected('invalid_dpop_proof', 'typ');
  }
  final jwk = header['jwk'];
  if (header['alg'] != 'ES256' || jwk is! JsonMap) {
    return const DpopRejected('invalid_dpop_proof', 'alg or jwk');
  }
  if (jwk.containsKey('d')) {
    return const DpopRejected('invalid_dpop_proof', 'private key in jwk');
  }
  final signingInput = Uint8List.fromList(
    ascii.encode('${parts[0]}.${parts[1]}'),
  );
  if (!_verifyEs256(jwk, signingInput, signature)) {
    return const DpopRejected('invalid_dpop_proof', 'signature');
  }
  final jkt = thumbprint(jwk);
  if (expectedJkt != null && jkt != expectedJkt) {
    return const DpopRejected(
      'invalid_dpop_proof',
      'key is not the bound key',
    );
  }
  if (payload['htm'] != method.toUpperCase()) {
    return const DpopRejected('invalid_dpop_proof', 'htm');
  }
  if (payload['htu'] != htu) {
    return const DpopRejected('invalid_dpop_proof', 'htu');
  }
  if (accessToken != null && payload['ath'] != athOf(accessToken)) {
    return const DpopRejected('invalid_dpop_proof', 'ath');
  }
  final iat = payload['iat'];
  final t = now().millisecondsSinceEpoch ~/ 1000;
  if (iat is! int ||
      iat < t - maxAge.inSeconds - leeway.inSeconds ||
      iat > t + leeway.inSeconds) {
    return const DpopRejected('invalid_dpop_proof', 'iat');
  }
  final nonce = payload['nonce'];
  final accepted = nonces ?? {_current, ?_previous};
  if (nonce is! String || !accepted.contains(nonce)) {
    return const DpopRejected('use_dpop_nonce', 'nonce missing or stale');
  }
  final jti = payload['jti'];
  if (jti is! String || jti.isEmpty) {
    return const DpopRejected('invalid_dpop_proof', 'jti');
  }
  if (!_seen.add('$htu|$jti')) {
    return const DpopRejected('invalid_dpop_proof', 'jti reused');
  }
  return DpopAccepted(
    jkt,
    previousNonce: nonces == null && nonce != _current,
  );
}