Verify Webhook Receipt Payload
When the Consent Manager sends a webhook notification, the payload includes a cryptographically signed receipt.
To ensure authenticity and integrity, you must verify:
- Signature (JWS verification)
- Payload integrity (hash validation)
๐ Why Verification is Requiredโ
Webhook payloads are signed to ensure:
- The data is issued by Consent Manager
- The payload is not tampered with
- The consent receipt is authentic and trustworthy
๐ Verification Flowโ
- Fetch JWKS (public keys) from Consent Manager
- Extract
kid(Key ID) from JWS header - Find the matching public key
- Verify the JWS signature
- Validate payload hash
๐ Step 1: Fetch Public Keys (JWKS)โ
Consent Manager exposes public keys via JWKS endpoint:
- Staging:
https://staging.indiaconsent.com/.well-known/jwks.json - Production:
https://www.indiaconsent.com/.well-known/jwks.json
These keys are used to verify webhook signatures.
๐งพ Step 2: Extract JWS from Webhookโ
The webhook contains a JWS compact string, for example:
<HEADER>.<PAYLOAD>.<SIGNATURE>
You will use this to:
- Extract header (to get
kid) - Verify signature
- Read payload
๐ Step 3: Identify Correct Public Keyโ
- Parse the JWS header
- Extract
kid(Key ID) - Match it with the corresponding key from JWKS
โ Step 4: Verify JWS Signatureโ
- Ensure algorithm is RS256
- Use the matching RSA public key
- Verify the signature
If verification fails โ Reject the webhook
๐ Step 5: Validate Payload Integrityโ
- Compute SHA-256 hash of the JWS payload
- Compare it with the receipt payload hash received separately
If mismatch โ Reject the payload
Sample Codeโ
- Java
- Python
- NodeJS
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.interfaces.RSAPublicKey;
import java.util.HexFormat;
import java.util.Optional;
public class JwsVerifyUsingJWKS {
public static void main(String[] args) throws Exception {
// 1๏ธโฃ Read JWKS JSON file
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://staging.indiaconsent.com/.well-known/jwks.json"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String jwksJson = response.body();
// 2๏ธโฃ Parse JWKS
JWKSet jwkSet = JWKSet.parse(jwksJson);
// 3๏ธโฃ JWS compact (received from CM)
String jwsCompact = "eyJraWQiOiJjbS1yc2Eta2...";
// 4๏ธโฃ Parse JWS header to get kid
JWSObject jwsObject = JWSObject.parse(jwsCompact);
String kid = jwsObject.getHeader().getKeyID();
// 5๏ธโฃ Find matching JWK by kid
Optional<JWK> jwkOpt = jwkSet.getKeys()
.stream()
.filter(jwk -> kid.equals(jwk.getKeyID()))
.findFirst();
if (jwkOpt.isEmpty()) {
throw new RuntimeException("No matching key found for kid: " + kid);
}
if (!JWSAlgorithm.RS256.equals(jwsObject.getHeader().getAlgorithm())) {
throw new SecurityException("Unexpected JWS algorithm");
}
// 6๏ธโฃ Convert to RSAPublicKey
RSAKey rsaKey = (RSAKey) jwkOpt.get();
if (!"RSA".equals(rsaKey.getKeyType().getValue())) {
throw new SecurityException("Invalid key type");
}
RSAPublicKey publicKey = rsaKey.toRSAPublicKey();
// 7๏ธโฃ Verify JWS
JWSVerifier verifier = new RSASSAVerifier(publicKey);
boolean isValid = jwsObject.verify(verifier);
// 8๏ธโฃ Output
System.out.println("Verification result: " + isValid);
System.out.println("Using key ID: " + kid);
System.out.println("Signed payload: " + jwsObject.getPayload());
String receiptPayloadHash = "6cfaf1d741a62828ed4...";
String payloadHash = sha256Hex(jwsObject.getPayload().toString());
if (receiptPayloadHash.equals(payloadHash)) {
System.out.println("Receipt Payload Hash matching with JKS Payload Hash");
} else {
throw new SecurityException("Payload Hash not matching");
}
}
private static String sha256Hex(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
throw new RuntimeException("Error calculating SHA-256", e);
}
}
}
import requests
import jwt
import hashlib
import base64
from jwt.algorithms import RSAAlgorithm
JWKS_URL = "https://staging.indiaconsent.com/.well-known/jwks.json"
def fetch_jwks():
response = requests.get(JWKS_URL)
response.raise_for_status()
return response.json()
def find_jwk_by_kid(jwks, kid):
for key in jwks["keys"]:
if key.get("kid") == kid:
return key
return None
def sha256_hex(input_str):
return hashlib.sha256(input_str.encode("utf-8")).hexdigest()
def base64url_decode(input_str):
padding = '=' * (-len(input_str) % 4) # Fix padding
return base64.urlsafe_b64decode(input_str + padding)
def verify_jws(jws_compact, expected_payload_hash):
# 1๏ธโฃ Fetch JWKS
jwks = fetch_jwks()
# 2๏ธโฃ Decode header (without verification)
unverified_header = jwt.get_unverified_header(jws_compact)
kid = unverified_header.get("kid")
alg = unverified_header.get("alg")
print("Using key ID:", kid)
# 3๏ธโฃ Validate algorithm
if alg != "RS256":
raise Exception("Unexpected JWS algorithm")
# 4๏ธโฃ Find matching JWK
jwk = find_jwk_by_kid(jwks, kid)
if not jwk:
raise Exception(f"No matching key found for kid: {kid}")
if jwk.get("kty") != "RSA":
raise Exception("Invalid key type")
# 5๏ธโฃ Convert JWK โ Public Key
public_key = RSAAlgorithm.from_jwk(jwk)
# 6๏ธโฃ Verify JWS + decode payload
payload = jwt.decode(
jws_compact,
public_key,
algorithms=["RS256"],
options={"verify_aud": False}
)
print("Verification result: True")
print("Signed payload:", payload)
# 7๏ธโฃ Validate payload hash
payload_base64 = jws_compact.split('.')[1]
payload_bytes = base64url_decode(payload_base64)
payload_str = payload_bytes.decode("utf-8")
payload_hash = sha256_hex(payload_str)
if payload_hash == expected_payload_hash:
print("Receipt Payload Hash matching with JWS Payload Hash")
else:
raise Exception("Payload Hash not matching")
# ๐ฝ Example Usage
jws_compact = "eyJraWQiOiJjbS1yc2..."
expected_hash = "6cfaf1d741a628..."
verify_jws(jws_compact, expected_hash)
const jwt = require("jsonwebtoken");
const jwkToPem = require("jwk-to-pem");
const crypto = require("crypto");
async function verifyPayload() {
// 1๏ธโฃ Load JWKS
const jwksUrl = "https://staging.indiaconsent.com/.well-known/jwks.json";
try {
const response = await fetch(jwksUrl);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const jwksJson = await response.json();
console.log("JWKS:", jwksJson);
// 2๏ธโฃ JWS Compact
const jwsCompact = "eyJraWQiOiJjbS1yc2Eta2V5I...";
// 3๏ธโฃ Decode header
const decodedHeader = jwt.decode(jwsCompact, { complete: true }).header;
const kid = decodedHeader.kid;
const alg = decodedHeader.alg;
if (alg !== "RS256") {
throw new Error("Unexpected JWS algorithm");
}
// 4๏ธโฃ Find matching key
const matchingKey = jwksJson.keys.find(k => k.kid === kid);
if (!matchingKey) {
throw new Error("No matching key found for kid: " + kid);
}
// 5๏ธโฃ Convert JWK โ PEM
const publicKeyPem = jwkToPem(matchingKey);
// 6๏ธโฃ Verify signature
const verifiedPayload = jwt.verify(jwsCompact, publicKeyPem, {
algorithms: ["RS256"]
});
console.log("Verification successful");
console.log("Signed payload:", verifiedPayload);
// 7๏ธโฃ Verify SHA256
const receiptPayloadHash = "28e153259a1e16640e47...";
const payloadString = JSON.stringify(verifiedPayload);
const hash = crypto.createHash("sha256")
.update(payloadString)
.digest("hex");
if (hash === receiptPayloadHash) {
console.log("Receipt Payload Hash matches");
} else {
throw new Error("Payload Hash not matching");
}
} catch (error) {
console.error("Error fetching JWKS:", error);
throw error;
}
}
verifyPayload();
โ ๏ธ Important Security Checksโ
Always enforce the following:
- โ Accept only HTTPS requests
- โ Verify JWS signature (RS256 only)
- โ Validate kid โ public key mapping
- โ Validate payload hash
- โ Ensure idempotency (avoid duplicate processing)
- โ
Match
requestIdwith your records
๐ซ When to Reject the Webhookโ
Reject the request if:
- Signature verification fails
- No matching
kidfound - Algorithm is not RS256
- Payload hash mismatch
- Invalid or missing fields
๐ Summaryโ
Webhook verification ensures that:
- The payload is authentic
- The data is untampered
- The consent decision is trusted
Always verify both signature + payload hash before processing or storing consent data.