SaaSquatch Help Center

SaaSquatch will “sign” webhook requests so that you can verify that the webhook request was generated by SaaSquatch, and not from some server acting like SaaSquatch.

Since the signature is specific to each and every webhook request, it also helps you validate that the message wasn’t intercepted and modified by someone in between you and SaaSquatch (i.e. Man-in-the-middle attack).

SaaSquatch includes two signatures; the X-Hook-JWS-RFC-7797 header is a JSON Web Signature (JWS) that supports key rotation and is the recommended approach for verifying webhooks authenticity.

Security Headers

Careful! Although you can verify the hook's authenticity via the signature, you still may need to verify the state of the 'data' by making an API call. Hook delivery order is not guaranteed.

For example, consider the scenario where an object is updated multiple times in quick succession. The related REST hooks may be delivered in a different order than the update events which generated them, so relying on their contents may lead you to build a different final state.

🔗 How the signature works

The X-Hook-JWS-RFC-7797 signature is a JSON Web Signature (JWS) based on RFC-2104 and has a detached payload. It is a string that looks like JWSHeader..JWSSignature.

JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based [RFC7159] data structures. The JWS cryptographic mechanisms provide integrity protection for an arbitrary sequence of octets. See Section 10.5 for a discussion on the differences between digital signatures and MACs.

The signature generation works as follows:

  • Webhook data is generated (e.g. a reward.created webhook)
  • The payload is signed using one of the keys from SaaSquatch's JSON Web Key Set (e.g. kid: 94ab304d-c90a-45ba-80e4-b4516a57a1c8)
  • The JWS header will contain some standard properties:
    • kid - The key used to sign the request. This can be looked up in the JWKS
    • alg - Will be RS256
    • typ - Will be JWT
  • The JWS will be added to as the X-Hook-JWS-RFC-7797 header to the Webhook request
  • The HTTP Request is sent as a POST to all the Webhook endpoints subscribed
graph TD A-->B-->C-->D A["Webhook Generated (e.g. reward.created)"] B[Payload Base64Encoded] C[Signature Generated using JWKS] D[HTTP Request Sent]

🔗 Verifying a Webhook payload

To verify that a webhook did actually come from SaaSquatch, you need to compare the JSON Web Signature (JWS) from the X-Hook-JWS-RFC-7797 header with the JSON body of the request:

  1. Ensure the X-Hook-JWS-RFC-7797 header exists. If it doesn't exist, this request didn't come from SaaSquatch.
  2. Look up the SaaSquatch JWKS at http://app.referralsaasquatch.com/.well-known/jwks.json. This contains the public keys, and should have a kid that matches the JWS Header of the JWS. The JWKS changes infrequently and is safe to cache.
  3. Grab the JSON body from the request. This should always be JSON.
  4. Use a JWT library for your programming language to verify the body matches the signature. The JWS signature uses a detached payload, so it is of the form JWSHEADER..JWSSIGNATURE. To implement the verification, some languages may require you to build Base64 Encode the JWS Payload (e.g. the webhook body) in order to verify the JWS.

These libraries support RFC-7797 and JWKS, and simplify verifying a JWS:

graph TD A-- Yes -->B-->C-->D-->E-- Yes -->Y A-- No -->N E-- No -->N A{{`X-Hook-JWS-RFC-7797` header exists?}} B[Look up the SaaSquatch JWKS] C[Grab the JSON body from the request] D[Validate with JWT library] E{{Body matches Signature?}} N(Reject / Invalid) Y(Valid) style Y fill:#6F6; style N fill:#F66;

🔗 JSON Web Key Set (JWKS)

JSON Web Key Set (JWKS) is a standard for sharing crytographic keys. You can find the JSON Web Key set for most services on the /.well-known/jwks.json url.

For SaaSquatch the JSON Web Key Set is located at: http://app.referralsaasquatch.com/.well-known/jwks.json

Our JWKS contains the public keys of the public/private key pairs used for asymmetric encryption.

Asymmetric encryption - The X-Hook-JWS-RFC-7797 webhook that we generate is signed using asymmetric encryption. That means that it is signed with a private key known only to SaaSquatch, but that the signature can be verified by anyone using the matching private key from the JWKS.

🔗 JWS vs JWT

JWT actually uses JWS for its signature, from the spec:

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JavaScript Object Notation (JSON) object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or MACed and/or encrypted.

In other words, a JWT is a JWS structure with a JSON object as the payload. Some optional keys (or claims) have been defined such as iss, aud, exp etc.

This also means that its integrity protection is not just limited to shared secrets but public/private key cryptography can also be used.

  • JWS - JSON Web Signature
  • JWT - JSON Web Token

🔗 Example Webhook

Show example Webhook json Accept-Encoding: gzip,deflate,br Content-Type: application/json; charset=UTF-8 Content-Length: 543 Connection: keep-alive X-Hook-JWS-RFC-7797: eyJraWQiOiIzZDMxM2JjOC1hYjNiLTRmM2MtYWJiNy0zN2I4NGE0MmQwZGEiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..DQfCOrdudxqz4r7uiCAhyKIi4bGZignWmr1ct_7Bf6DXmgwUciQJaQTvYffc5lni9K6DqclQG0cfI6X5pqceeFays1_atEP-bsN6w_0krjKg72rcVHKecgEOlFNhsF0xfYdjoY-5z-tpzpjOU1QBKOl7eE8K9AkCL5FDg6Huu26Ov1TcmEGhNMSN7UW0zBNXvNsjeRfO57dKgtA-6wyl3TUcsxYsz81Q3Og0dprMfNBr-bcqvs4aHUUxLmU013RYXAdQmK395NvN54YJniZcsy8svF1THExp4WkmOw9WmX_kHUhsvadTegAI4PbGYx9h1xIcdV_IrfuzUV1Ta9WfKg X-Hook-Signature: h2JX9dV4o1r2sJypeVBIWOqW0as= { "id": "5dfaadc9d132f00f8b742288", "type": "reward.created", "tenantAlias": "a5kz4dlxt403z", "live": true, "created": 1576709577227, "data": { "type": "CREDIT", "id": "577405e3e4b0cc57c1e2e684", "dateCreated": 1467221475151, "dateScheduledFor": null, "dateGiven": 1467221475151, "dateExpires": 1475170275151, "dateCancelled": null, "accountId": "6UTR8OQZX0HE3QBP", "userId": "56f2e6a9e4b08a1cbef6c561", "cancellable": true, "rewardSource": "FRIEND_SIGNUP", "programId": null, "unit": "%", "assignedCredit": null, "redeemedCredit": null, "name": null, "currency": null, "redemptions": null } }

🔗 Java Code Example

Below is an example of validating the JSON Web Signature of a Webhook request using Java.

Show Java Source example java import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; class Webhooks{ public String validateSquatchWebhook(String sigHeader, byte[] bodyBytes) { if (StringUtils.isBlank(sigHeader)) return "signature missing"; final String jwtStr = StringUtils.replaceOnce(sigHeader, "..", '.' + Base64.getUrlEncoder().withoutPadding().encodeToString(bodyBytes) + '.'); final SignedJWT signedJWT; try { signedJWT = SignedJWT.parse(jwtStr); } catch (ParseException e) { return "Invalid JWT"; } final JWKSet jwks = JWKSet.load("https://app.referralsaasquatch.com/.well-known/jwks.json"), 2500, 5000, 0); final RSAKey jwk = (RSAKey) jwks.getKeyByKeyId(signedJWT.getHeader().getKeyID()); if (jwk == null) { return "jwk not found for kid"; } final JWSVerifier verifier; try { verifier = new RSASSAVerifier(jwk); } catch (JOSEException e) { throw new RuntimeException(e); // internal error } final boolean verifyResult; try { verifyResult = signedJWT.verify(verifier); } catch (JOSEException e) { throw new RuntimeException(e); // internal error } if (!verifyResult) { return "Invalid JWT signature"; } return null; } }