verify method
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,
);
}