diff --git a/src/main/kotlin/com/nylas/util/WebhookRequestValidator.kt b/src/main/kotlin/com/nylas/util/WebhookRequestValidator.kt new file mode 100644 index 00000000..d97bfbc1 --- /dev/null +++ b/src/main/kotlin/com/nylas/util/WebhookRequestValidator.kt @@ -0,0 +1,49 @@ +package com.nylas.util + +import java.nio.charset.StandardCharsets +import java.security.GeneralSecurityException +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * Utility class to validate the signature of a webhook request. + */ +object WebhookRequestValidator { + + /** + * Validates the signature of a webhook request + * + * @param payload the raw payload of the request + * @param webhookSecret your webhook secret key + * @param expectedSignature the expected signature of the request. (Header "X-Nylas-Signature") + * @return true if the request is valid + */ + @JvmStatic + fun isRequestValid(payload: String, webhookSecret: String, expectedSignature: String): Boolean { + return try { + val expectedBytes = hexStringToByteArray(expectedSignature) + val mac = Mac.getInstance("HmacSHA256") + val secretKeySpec = SecretKeySpec(webhookSecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256") + mac.init(secretKeySpec) + val sigBytes = mac.doFinal(payload.toByteArray(StandardCharsets.UTF_8)) + expectedBytes contentEquals sigBytes + } catch (e: GeneralSecurityException) { + throw RuntimeException(e) + } + } + + /** + * Converts a hex string to a byte array + * + * @param s the hex string to convert + * @return the byte array + */ + private fun hexStringToByteArray(s: String): ByteArray { + val len = s.length + val data = ByteArray(len / 2) + for (i in 0 until len step 2) { + data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() + } + return data + } +} diff --git a/src/test/kotlin/com/nylas/util/WebhookRequestValidatorTest.kt b/src/test/kotlin/com/nylas/util/WebhookRequestValidatorTest.kt new file mode 100644 index 00000000..4847b960 --- /dev/null +++ b/src/test/kotlin/com/nylas/util/WebhookRequestValidatorTest.kt @@ -0,0 +1,24 @@ +package com.nylas.util + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class WebhookRequestValidatorTest { + + @Test + fun isRequestValid() { + val webhookSecret = "UfZWFrkjsbg5bxLbax-y" + val payload = """{"specversion":"1.0","type":"grant.deleted","source":"/nylas/system","id":"Kkx2QQB7TWjWM3KXRQQn562394","time":1717162394,"webhook_delivery_attempt":1,"data":{"application_id":"a7966b5a-9bc2-44b7-9895-4e2b281ce6be","object":{"code":25013,"grant_id":"4c22c3c5-d163-48b6-ad95-45fedc879683","integration_id":"b7dd6e6e-af6a-463b-92fa-be35c2683f5c","metric":1934,"provider":"google"}}}""" + + // When the signature header value is valid + val validSigHeader = "a4534f7e68dcd54f0621f7539ddd5a0d14a9a46ace9b68a4cfd9c99b4b6b89cc" + // Then isRequestValid() returns true + assertTrue(WebhookRequestValidator.isRequestValid(payload, webhookSecret, validSigHeader)) + + // When the signature header value is invalid + val invalidSigHeader = "a4534f7e68dcd54f0621f7539ddd5a0d14a9a46ace9b68a4cfd9c99b4b6b89ca" + // Then isRequestValid() returns false + assertFalse(WebhookRequestValidator.isRequestValid(payload, webhookSecret, invalidSigHeader)) + } +}