Skip to content

Commit

Permalink
Add WebhookRequestValidator - a utility class that provides request v…
Browse files Browse the repository at this point in the history
…alidation by providing the request payload, webhook secret, and request signature.
  • Loading branch information
burtonrhodes committed Jun 28, 2024
1 parent bc67e7d commit 6608d7e
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/main/kotlin/com/nylas/util/WebhookRequestValidator.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions src/test/kotlin/com/nylas/util/WebhookRequestValidatorTest.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}

0 comments on commit 6608d7e

Please sign in to comment.