Keep webhooks secure
Webhook authentication means validating every Otter webhook before you trust the payload. This guide is the deep how-to for HMAC and Authorization options.
For the full auth picture (API tokens and webhooks), start with Authentication. For REST vs webhooks, ack, and error callbacks, see Events and webhooks.
Overview
Each webhook endpoint has a secret and an Authentication Type.
The Authentication Type controls the HTTP Authorization header Otter sends to your service. Choose one of: HMAC SHA1 (legacy), Basic Auth, Bearer Token, or None (no Authorization header).
Independent of that choice, every request also includes an X-HMAC-SHA256 header: the SHA-256 HMAC of the request body using your endpoint secret. Always validate that header.
HMAC SHA256 signature
Every webhook request is signed with HMAC-SHA256. The signature is in the X-HMAC-SHA256 header.
Validate this signature on every request before you process the body.
HMAC-SHA256 proves the request came from Otter and that the body was not altered. An attacker who only sees the hash can replay the same request, but cannot forge new payloads without the secret.
Example of HTTP Headers (SHA256)
Headers from a request using the Bearer Token Authentication type.
Please, notice the x-hmac-sha256 header, this example shows that you
will receive this header independent of the selected Authentication Type.
content-length: 298
x-hmac-sha256: PLZ05+ixPce3G/cKhiausM7ZGbmpISyzcnP0ivaPju4=
authorization: Bearer token123
content-type: application/json
Computing and validating the hash (SHA256)
The HMAC SHA256 is computed using the following algorithm:
- Encode the request body and the secret to UTF-8
- Compute the HMAC SHA256 hash using previous values, this will give you a byte-array
- Encode the byte-array to base64 and decode the result to UTF-8
- The result will be a string with the base64 encoding of the HMAC SHA256 signature
You can always get the Webhook secret for your endpoint in the Otter Developer Portal.
To validate the hash:
- Compute the hash using the method above
- Extract the hash from the header
X-HMAC-SHA256 - Compare both strings
Reference implementations:
import base64
import hmac
import hashlib
def compute_hash(webhook_secret: str, payload: str, algorithm = hashlib.sha256) -> str:
webhook_secret_bytes = webhook_secret.encode('UTF-8')
payload_bytes = payload.encode('UTF-8')
computed_hash_bytes = hmac.new(webhook_secret_bytes, payload_bytes, algorithm).digest()
computed_hash_base64_bytes = base64.encodebytes(computed_hash_bytes)
return computed_hash_base64_bytes.decode('UTF-8').strip()
def validate(webhook_secret: str, payload: str, hash_signature: str, algorithm = hashlib.sha256) -> bool:
computed_hash = compute_hash(webhook_secret, payload, algorithm)
return hmac.compare_digest(computed_hash, hash_signature)
Hints
This online tool is a good place to validate your implementation. Just ensure you select the output format as base64.
Legacy Authentication: HMAC SHA1
The legacy authentication type is a HMAC SHA1 of the request body and the Webhook's secret. It sends in every request an HTTP Header with this format: Authorization: MAC {HASH}.
SHA-1 is weak. Prefer validating
X-HMAC-SHA256(and avoid relying on legacy HMAC SHA1 auth).
Computing and validating the hash (SHA1)
To compute the hash you can use the same code from the HMAC SHA256, just change the algorithm to hmac.sha1.
To validate the request, extract the hash from the header Authorization. IMPORTANT:
Remember to remove the MAC prefix from the header and always strip your strings.
Example of HTTP Headers (SHA1)
x-hmac-sha256: TLUkvaPA7J+FWuQXDwcgnLa84WuHp526pCt4I6FgsXk=
authorization: MAC LblnjLxrJr40CDcM44+cvM/dYlk=
Basic Auth
If you use this Authentication Type, you will need to provide a username and password in the endpoint configuration in the Otter Developer Portal. You can get more details about basic auth here.
Webhooks with Basic Auth will have this HTTP Header: Authorization: Basic base64({username}:{password}).
If an attacker intercepts Basic Auth credentials, they can forge requests with those credentials.
Always validate
X-HMAC-SHA256as well. Prefer not to rely on Basic Auth alone.
Validating (Basic Auth)
- Extract the
Authorizationheader value - Remove the
Basicprefix - Decode the value using base64
- Split the string in the first
:, the first value will be the username and the remaining the password - Validate the username and password with value stored locally in your application
Example of HTTP Headers (Basic Auth)
x-hmac-sha256: TLUkvaPA7J+FWuQXDwcgnLa84WuHp526pCt4I6FgsXk=
authorization: Basic dGVzdGU6dGVzdGU=
Bearer Token
Very similar to Basic Auth but uses a user defined token instead of username and password. You are free to use anything that makes sense for your application as a token, but a JWT is a good approach.
If an attacker intercepts a Bearer token, they can reuse it until you rotate it.
Always validate
X-HMAC-SHA256as well. Prefer not to rely on Bearer auth alone.
Validating (Bearer Token)
- Extract the
Authorizationheader value - Remove the
Bearerprefix - Compare the token with your internal one
Example of HTTP Headers (Bearer Token)
x-hmac-sha256: TLUkvaPA7J+FWuQXDwcgnLa84WuHp526pCt4I6FgsXk=
authorization: Bearer this.is.a.token