Skip to main content

Signature token verification for Webhook events

Context

This document is designed to explain the current pipeline for Signature token verification for myKaarma webhook events.

API partners can take a look at the details about the secret key and the algorithm used for generating the Signature token, a sample cURL for an event received with the signature token in header, and a pseudocode for a Java Spring Boot webhook consumer endpoint with the signature token verification code.

Need for Signature verification

To ensure that your server only processes webhook deliveries that were sent by myKaarma, and to ensure that the delivery was not tampered with, you should validate the webhook signature before processing the delivery further.
This will help you avoid spending server time to process deliveries that are not from myKaarma and will help avoid man-in-the-middle attacks.

Secret key used for signature token generation

All webhook events sent by myKaarma include a myKaarma-signature-token header. We generate this header using a secret key that only you and myKaarma know, and will be shared with you beforehand.
Currently, the secret key used for the signature token generation process is the same as the username shared with you for the basic auth credentials used for myKaarma APIs access. In the meantime, we're working on making this process more robust from a security perspective, which will involve a different strategy for selection of the secret key.

Algorithm used for signature token generation

Currently, we use the HMAC SHA256 algorithm for generating the signature token. The algorithm name is also sent pre-pended to the signature token header, so the value for the myKaarma-signature-token received on your end will look something like: sha256=GENERATED_SIGNATURE_TOKEN

Sample cURL for an event

Below you'll find the cURL for a sample API request received on a webhook consumer endpoint. The signature token in header is generated with HMAC SHA256 algorithm using a sample secret key SampleSecretKey

curl -X 'POST' 'https://webhook.site/f01f9f2c-96c8-4f1f-82f7-fe8e9f43dbba' -H 'accept-encoding: gzip,deflate' -H 'host: webhook.site' -H 'content-length: 2570' -H 'content-type: text/plain' -H 'range: bytes=0-1048575' -H 'mykaarma-signature-token: sha256=q0vrkr2Z8MA1yBIPERIj8xeN5qmX_qJWiKgPpXBZS_M' -H 'user-agent: Amazon/EventBridge/ApiDestinations' -H 'authorization: Basic YWRtaW46ekRHeTRrMmJxVUVoajh3aVRva0w=' -H 'php-auth-user: admin' -H 'php-auth-pw: zDGy4k2bqUEhj8wiTokL' -d $'{"id":"123456789","dealeruuid":"cb731d36fd635ddd6ef8dd43500892b0c0249d1c01a46dbcc445a809c0a8e3b2","payload":"{\\"uuid\\":\\"7edd41ca-0894-4eed-b349-d3492a917aac\\",\\"departmentUuid\\":\\"95a99dda6af8da904fe130db4ff0e2613f0f3ec96784fb4e4ac5d3cbfa488cb5\\",\\"subscriberName\\":\\"mkAppointmentPushAdapter\\",\\"serviceAppointmentRequestLoanerBookingList\\":[],\\"event\\":\\"UPDATED\\",\\"appointmentInfoResponse\\":{\\"uuid\\":\\"ylV70RqRl_vQxbx8TGsS2kpYUvcrmyTLy8vr2p-vMuU\\",\\"customerInformation\\":{\\"firstName\\":\\"TEST\\",\\"lastName\\":\\"myKAARMA\\",\\"company\\":null,\\"customerKey\\":null,\\"uuid\\":\\"Tdcj_nYjMjHrjzwe_I0Kyx5LYdSKQMz7V0jLGTmTW08\\"},\\"vehicleInformation\\":{\\"vin\\":null,\\"vehicleKey\\":null,\\"model\\":\\"Odyssey\\",\\"year\\":\\"2005\\",\\"brand\\":\\"Honda\\",\\"trim\\":\\"Touring\\",\\"engine\\":null,\\"mileage\\":null,\\"dealerUuid\\":null,\\"uuid\\":\\"5uIXd8mfzogn79V6V0N-KP300-VItMm2ahKGpEoNPdc\\"},\\"orderInformation\\":{\\"uuid\\":null,\\"orderNumber\\":null,\\"orderDate\\":null,\\"orderType\\":null},\\"dealerUuid\\":\\"ee69120e3f814de6bf033f25ad7d534e10a459d322a9d9d44933d49e57037484\\",\\"assignedAdvisorUuid\\":\\"927d23d2482b3ecbb42e45085341ebc659381464ac27e3bdda7a704efcc5fcc4\\",\\"creatorAdvisorUuid\\":null,\\"date\\":\\"2024-04-04 00:48:23\\",\\"startTime\\":\\"2024-04-04 11:00:00\\",\\"endTime\\":\\"2024-04-04 11:14:59\\",\\"transportOption\\":{\\"uuid\\":\\"CjaK5wAvBDG7Di6XAt6RpllbGtz9SiHoBzM2vDiFa9Q\\",\\"altTransportation\\":\\"Will Wait\\",\\"bookingId\\":null,\\"bookInThirdParty\\":false,\\"bookingStartDate\\":null,\\"bookingEndDate\\":null,\\"bookingIsManual\\":null,\\"bookingIsValid\\":null,\\"transportation\\":\\"Will Wait\\",\\"organicTransportation\\":null},\\"appointmentKey\\":\\"2054902924957\\",\\"mileageText\\":null,\\"recall\\":false,\\"appointmentSource\\":\\"Online Scheduler\\",\\"status\\":\\"N\\",\\"comments\\":\\"\\",\\"internalNotes\\":null,\\"isCancelled\\":null,\\"reminderCount\\":0,\\"serviceList\\":[],\\"skillList\\":null,\\"appointmentCommunicationPreferences\\":{\\"emailConfirmation\\":true,\\"textConfirmation\\":true,\\"emailReminder\\":true,\\"textReminder\\":true,\\"confirmationEmail\\":null,\\"confirmationPhoneNumber\\":null,\\"notifyCustomer\\":true,\\"sendCommunicationToDA\\":true},\\"pdrToOpcodes\\":null,\\"estimatePdfS3Url\\":null,\\"customerSignatureUrl\\":null,\\"signatureRequestId\\":null,\\"signatureCaptureUrl\\":null,\\"signedEstimatePdfUrl\\":null,\\"estimateSignatureStatus\\":null,\\"teamUuid\\":\\"BPQaaymA5V15mZL9UttBwqIf9wIDcWrNapOlhDpAeNs\\",\\"customerVehicleInspectionId\\":null,\\"sarCheckinDataDTO\\":null},\\"eventCreationDateTime\\":\\"2024-04-03 23:48:45.790\\",\\"updatedByUserUuid\\":null}","timestamp":1649095200000,"type":"appointments"}'
Signature token provided in header

Verification process for received signature token

To verify that the event received on your webhook endpoint is a genuine event from myKaarma, you can use the secret key to generate your own signature for each webhook.
Since only you and myKaarma know the secret key, if both signatures match, you can be sure that a received event came from myKaarma.

Pseudocode for a Java Spring Boot webhook consumer endpoint

@RestController
@RequestMapping("/webhook")
public class WebhookController {

@PostMapping(value = "/mykaarma-events", produces = "application/json")
public ResponseEntity<Void> myKaarmaEventsWebhook(@RequestBody String event,
@RequestHeader("myKaarma-signature-token") String signatureToken) {
try {
verifySignatureHeader(event, signatureToken);

// process event asynchronously now
// ...
} catch (Exception e) {
log.error(" Some error occurred while verifying signature header for myKaarma event ", e);
}

return new ResponseEntity<Void>(HttpStatus.OK);
}


private void verifySignatureHeader(String payload, String receivedSignatureHeader) throws Exception {
try {
String secret = fetchSecretKey(); //this should fetch the secret key configured for webhooks signature token generation

if (receivedSignatureHeader == null || receivedSignatureHeader.isBlank()) {
log.warn(" In verifySignature ---- Received a blank signature token header ");
throw new SignatureVerificationException(" In verifySignature ---- Received a blank signature token header ", receivedSignatureHeader);
}

String algo = "", receivedSignature = "";
String[] receivedSignatureParts = receivedSignatureHeader.split("=");
if (receivedSignatureParts.length >= 2) {
algo = receivedSignatureParts[0];
receivedSignature = receivedSignatureParts[1];
}
if (algo == null || algo.isBlank() || receivedSignature == null || receivedSignature.isBlank()) {
log.warn(" In verifySignature ---- Received a blank signature token value = {} or algo = {} ", receivedSignature, algo);
throw new SignatureVerificationException(" In verifySignature ---- Received a blank signature token value or algo ", receivedSignatureHeader);
}
String computedSignature = "";
if ("sha256".equals(algo)) {
computedSignature = getComputedSignatureForHMACSHA256(jsonPayload, secret);
} //... similarly for other future algos

// Secure constant-time comparison
if ( java.security.MessageDigest.isEqual(computedSignature.getBytes(), receivedSignature.getBytes())) {
return true;
}

log.warn(" In verifySignature ---- No signatures found in received signatures = {} matching the expected signature = {} for payload ", receivedSignatureHeader, computedSignature);
throw new SignatureVerificationException(" In verifySignature ---- No signatures found in received signatures ", receivedSignatureHeader);
} catch (Exception e) {
log.warn(" Exception in verifySignature: ", e);
throw new SignatureVerificationException(" In verifySignature ---- Exception in verifySignature ", receivedSignatureHeader);
}
}
}
...
public class SignatureVerificationException extends Exception {

private static final long serialVersionUID = 2L;
private final String signatureHeader;

public SignatureVerificationException(String message, String signatureHeader) {
super(message);
this.signatureHeader = signatureHeader;
}
}

Important Notes

  • Signature token generated in myKaarma is stripped of padding. Make sure the expected signature token you generate on your end is also generated in a similar manner.
  • Don't transform or process the raw body of the request, including adding whitespace or applying other formatting, before verifying the signature token. This results in a different signed payload, meaning signatures won't match when you compare.
  • It is recommended to use a secure constant-time comparison method for comparing the expected and the received signature tokens, rather than String.equals or other similar alternatives, to counter a possible DDoS from a malicious entity. For eg. the above sample consumer compares the MessageDigest for the signatures.
  • Currently, we support only 1 signature token in header sent in the webhook event. However, keeping in mind the possibilities in the future - of (a) secret key rotation, and (b) signature token generation algorithm rotation - we might need to send multiple signature tokens in the event. In such a case, the format of the event/header might change somewhat. For a breaking change, we'll provide all webhook consumers a 2-week buffer to ensure they are updated to the newer format, before rolling it out.

Questions

What do I need to do to start receiving the signature token in header?

Nothing. All events received from myKaarma WILL contain the signature token in the header.

Can I get some custom headers added specific to my webhook?

Third-party partners can request additional static headers to be added to the webhook API request, as long as these do not conflict with any of the existing header names already being passed in the request.