Just noticed that our Python consumer rejects valid NICE CXone webhook events with a 403 Forbidden when re-processing queued items. The signature header X-NICE-Signature seems to include a timestamp that invalidates the HMAC calculation if the event is delayed. Is the signature bound to the exact second of delivery, or is there a tolerance window I should implement in the verification logic to handle network jitter?
This happens because the strict 5-minute validity window for the t parameter in the signature header.
- Ensure your consumer retrieves the timestamp from the header, not the system clock.
- Verify the HMAC matches the payload using the shared secret before checking the time delta.
- Reject the request if the absolute difference exceeds 300 seconds.
If you check the docs, they mention the timestamp validation is strict, but implementing it in a serverless environment introduces a different class of failure that isn’t immediately obvious.
Is the signature bound to the exact second of delivery, or is there a tolerance window I should implement in the verification logic to handle network jitter?
The 5-minute window is indeed the limit, but the real risk here is clock skew between your Lambda execution environment and the NICE CXone signing server. If your Lambda container has been idle for a while, the initial cold start might sync the clock, but subsequent invocations in the same container might drift slightly if the underlying host has issues. More critically, if you are processing these events from an SQS queue (as I assume you are, given the “queued items” mention), the event is no longer “live.” The timestamp t in the header is from the original webhook delivery, not the time Lambda processes the message.
If you process a message from SQS 10 minutes after the original webhook attempt failed, your verification logic will correctly reject it as expired. You cannot simply add a tolerance window because that defeats the purpose of replay attack protection.
Instead, you need to decouple the signature verification from the processing logic. Verify the signature at the point of ingestion (the API Gateway or ALB target), not inside the Lambda handler processing the queue. If the signature is valid at ingestion, store the payload in SQS. If it fails, send it to a Dead Letter Queue (DLQ) for manual inspection. Do not re-verify the signature when pulling from SQS.
Here is how I structure the verification in the initial Lambda trigger:
import hmac
import hashlib
import time
def verify_signature(payload: bytes, signature_header: str, secret: str) -> bool:
# Extract timestamp and signature
# Format: v1=signature&t=timestamp
parts = signature_header.split(',')
sig_map = {p.split('=')[0]: p.split('=')[1] for p in parts}
received_sig = sig_map['v1']
timestamp = int(sig_map['t'])
# Check time window (5 minutes = 300 seconds)
if abs(time.time() - timestamp) > 300:
return False
# Compute expected HMAC
expected_sig = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received_sig, expected_sig)
Once it passes this check and lands in SQS, treat it as trusted data. Re-verifying on replay is a waste of compute and will always fail if the delay exceeds 300 seconds.