Signature Verification
Verify webhook signatures to ensure requests originate from DocuRift
Every webhook request from DocuRift includes a cryptographic signature. Verifying this signature ensures the request genuinely came from DocuRift and hasn't been tampered with.
How Signatures Work
DocuRift signs each webhook request using HMAC-SHA256:
- We concatenate the timestamp and raw request body
- We compute an HMAC-SHA256 hash using your webhook secret
- We send the signature in the
X-DocuRift-Signatureheader
Request Headers
Each webhook request includes these security headers:
| Parameter | Type | Description |
|---|---|---|
X-DocuRift-Signature | string | HMAC-SHA256 signature of the request |
X-DocuRift-Timestamp | string | Unix timestamp when the request was sent |
X-DocuRift-Event-Id | string | Unique event identifier for deduplication |
Verification Steps
To verify a webhook signature:
- Extract the signature and timestamp from headers
- Construct the signed payload:
{timestamp}.{rawBody} - Compute HMAC-SHA256 using your webhook secret
- Compare your computed signature with the received signature
- Verify the timestamp is recent (within 5 minutes)
Use Raw Request Body
You must use the raw request body (not parsed JSON) for signature verification. Parsing and re-serializing JSON may change the byte representation.
Code Examples
Python
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_your_webhook_secret_here'
def verify_webhook_signature(payload: bytes, signature: str, timestamp: str) -> bool:
"""Verify the webhook signature from DocuRift."""
# Check timestamp is recent (within 5 minutes)
current_time = int(time.time())
request_time = int(timestamp)
if abs(current_time - request_time) > 300: # 5 minutes
return False
# Construct the signed payload
signed_payload = f"{timestamp}.".encode() + payload
# Compute expected signature
expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
signed_payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected_signature, signature)
@app.route('/webhooks/docurift', methods=['POST'])
def handle_webhook():
# Get raw body before parsing
payload = request.get_data()
# Get headers
signature = request.headers.get('X-DocuRift-Signature')
timestamp = request.headers.get('X-DocuRift-Timestamp')
if not signature or not timestamp:
abort(401, 'Missing signature headers')
if not verify_webhook_signature(payload, signature, timestamp):
abort(401, 'Invalid signature')
# Signature verified - process the event
event = request.json
print(f"Received event: {event['type']}")
return {'received': True}, 200
if __name__ == '__main__':
app.run(port=3000)Python (with Requests Library)
import hmac
import hashlib
import time
def verify_docurift_webhook(
raw_body: bytes,
signature: str,
timestamp: str,
secret: str
) -> bool:
"""
Verify a DocuRift webhook signature.
Args:
raw_body: The raw request body as bytes
signature: X-DocuRift-Signature header value
timestamp: X-DocuRift-Timestamp header value
secret: Your webhook secret (whsec_...)
Returns:
True if signature is valid, False otherwise
"""
# Reject old timestamps (replay attack prevention)
current_time = int(time.time())
request_time = int(timestamp)
if abs(current_time - request_time) > 300:
print("Timestamp too old, possible replay attack")
return False
# Build the signed payload
signed_payload = f"{timestamp}.".encode() + raw_body
# Calculate expected signature
expected = hmac.new(
secret.encode('utf-8'),
signed_payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)
# Example usage
webhook_secret = "whsec_abc123def456ghi789jkl012mno345pqr678"
raw_body = b'{"id":"evt_123","type":"document.processing.completed"}'
signature = "a1b2c3d4e5f6..." # From header
timestamp = "1706270400" # From header
is_valid = verify_docurift_webhook(raw_body, signature, timestamp, webhook_secret)
print(f"Signature valid: {is_valid}")Node.js (Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
const WEBHOOK_SECRET = process.env.DOCURIFT_WEBHOOK_SECRET;
function verifyWebhookSignature(payload, signature, timestamp) {
// Check timestamp is recent (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
const requestTime = parseInt(timestamp, 10);
if (Math.abs(currentTime - requestTime) > 300) {
console.error('Timestamp too old, possible replay attack');
return false;
}
// Construct the signed payload
const signedPayload = `${timestamp}.${payload}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
// Important: Use raw body parser for webhook route
app.post('/webhooks/docurift',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString();
const signature = req.headers['x-docurift-signature'];
const timestamp = req.headers['x-docurift-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing signature headers' });
}
if (!verifyWebhookSignature(payload, signature, timestamp)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature verified - process the event
const event = JSON.parse(payload);
console.log('Received event:', event.type);
// Acknowledge receipt immediately
res.status(200).json({ received: true });
// Process asynchronously
processEvent(event);
}
);
async function processEvent(event) {
switch (event.type) {
case 'document.processing.completed':
// Handle completed document
break;
case 'document.processing.failed':
// Handle failed document
break;
default:
console.log('Unhandled event type:', event.type);
}
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Node.js (TypeScript)
import { createHmac, timingSafeEqual } from 'crypto';
interface VerifyOptions {
payload: string | Buffer;
signature: string;
timestamp: string;
secret: string;
tolerance?: number; // seconds, default 300 (5 min)
}
export function verifyDocuRiftWebhook(options: VerifyOptions): boolean {
const { payload, signature, timestamp, secret, tolerance = 300 } = options;
// Validate timestamp
const currentTime = Math.floor(Date.now() / 1000);
const requestTime = parseInt(timestamp, 10);
if (isNaN(requestTime)) {
throw new Error('Invalid timestamp format');
}
if (Math.abs(currentTime - requestTime) > tolerance) {
throw new Error('Timestamp outside tolerance window');
}
// Build signed payload
const payloadStr = typeof payload === 'string' ? payload : payload.toString();
const signedPayload = `${timestamp}.${payloadStr}`;
// Compute expected signature
const expectedSignature = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
try {
return timingSafeEqual(
Buffer.from(expectedSignature, 'utf8'),
Buffer.from(signature, 'utf8')
);
} catch {
return false;
}
}
// Usage example
const isValid = verifyDocuRiftWebhook({
payload: rawRequestBody,
signature: headers['x-docurift-signature'],
timestamp: headers['x-docurift-timestamp'],
secret: process.env.DOCURIFT_WEBHOOK_SECRET!
});Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"math"
"net/http"
"strconv"
"time"
)
const webhookSecret = "whsec_your_webhook_secret_here"
func verifyWebhookSignature(payload []byte, signature, timestamp string) bool {
// Check timestamp is recent (within 5 minutes)
requestTime, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
currentTime := time.Now().Unix()
if math.Abs(float64(currentTime-requestTime)) > 300 {
return false
}
// Construct signed payload
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(payload))
// Compute expected signature
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write([]byte(signedPayload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Constant-time comparison
return hmac.Equal([]byte(expectedSignature), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-DocuRift-Signature")
timestamp := r.Header.Get("X-DocuRift-Timestamp")
if signature == "" || timestamp == "" {
http.Error(w, "Missing signature headers", http.StatusUnauthorized)
return
}
if !verifyWebhookSignature(payload, signature, timestamp) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Signature verified - process the event
fmt.Fprintf(w, `{"received": true}`)
}
func main() {
http.HandleFunc("/webhooks/docurift", webhookHandler)
http.ListenAndServe(":3000", nil)
}Security Best Practices
1. Always Verify Signatures
Never process webhook data without verifying the signature first. Unverified webhooks could be forged by attackers.
2. Use Constant-Time Comparison
Always use constant-time comparison functions (like hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js) to prevent timing attacks.
3. Validate Timestamps
Reject webhooks with timestamps older than 5 minutes to prevent replay attacks:
const TOLERANCE = 300; // 5 minutes in seconds
function isTimestampValid(timestamp) {
const currentTime = Math.floor(Date.now() / 1000);
const requestTime = parseInt(timestamp, 10);
return Math.abs(currentTime - requestTime) <= TOLERANCE;
}4. Protect Your Webhook Secret
- Store the secret in environment variables, not in code
- Never log or expose the secret
- Rotate the secret if it's ever compromised
5. Use HTTPS Only
Ensure your webhook endpoint uses HTTPS to protect the signature and payload in transit.
6. Handle Errors Gracefully
Return appropriate HTTP status codes:
| Status | Meaning | DocuRift Action |
|--------|---------|-----------------|
| 200 | Success | Event delivered |
| 401 | Invalid signature | Logs security event |
| 5xx | Server error | Retries delivery |
Testing Signature Verification
Use this test case to verify your implementation:
{
"secret": "whsec_test_secret_for_verification",
"timestamp": "1706270400",
"payload": "{"id":"evt_test","type":"document.processing.completed"}",
"expectedSignature": "8a4f7c3e9b2d1a6f5e4c3b2a1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e"
}Test Endpoint
Use the DocuRift dashboard's webhook testing feature to send test events and verify your signature implementation. See Testing Webhooks.
Troubleshooting
Signature Mismatch
If signature verification fails:
- Check the raw body: Ensure you're using the raw request body, not parsed JSON
- Verify the secret: Confirm you're using the correct webhook secret
- Check encoding: The payload should be UTF-8 encoded
- Inspect headers: Ensure headers are being read correctly
Timestamp Errors
If timestamp validation fails:
- Check server time: Ensure your server clock is synchronized (use NTP)
- Increase tolerance: Temporarily increase the tolerance window for debugging
- Log timestamps: Compare the request timestamp with your server time
Next Steps
- Testing Webhooks - Test your webhook integration
- Webhook Setup - Configure webhook endpoints
- Event Types - Learn about all event types