Skip to content

Commit

Permalink
Add WebhookRequestValidator to validate webhook requests (#238)
Browse files Browse the repository at this point in the history
I feel like this logic should definitely be in the SDK so that webhook
requests can be properly validated with out having to write custom code
to do it. The WebhookRequestValidator has one static method that
validates a webhook request given the request payload, webhook secret,
and request signature.

A companion test is included as well.

# License
<!-- Your PR comment must contain the following line for us to merge the
PR. -->
I confirm that this contribution is made under the terms of the MIT
license and that I have the authority necessary to make this
contribution on behalf of its copyright owner.

Co-authored-by: Mostafa Rashed <[email protected]>
  • Loading branch information
burtonrhodes and mrashed-dev authored Jun 28, 2024
1 parent f7c5553 commit 4bf0772
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 4bf0772

Please sign in to comment.