JWT token verification for webhook endpoints
company_uuid
- Company that triggered the eventuser_uuid
- User who performed the actionevent_id
- Event type identifierapi_key
- Your integration’s API keyexternal_settings
- Integration configurationPOST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"lead": {
"uuid": "lead_12345678-1234-5678-9012-123456789abc",
"company_name": "Acme Corp",
"email": "contact@acme.com"
},
"configuration": [
{
"id": "lead_integration_settings",
"selected": [
{
"id": "unique_field_lead",
"value": "email"
}
]
}
]
}
{
"company_uuid": "company_12345678-1234-5678-9012-123456789abc",
"user_uuid": "user_87654321-4321-8765-2109-876543210987",
"event_id": "lead_created",
"api_key": "jb_api_1234567890abcdef",
"external_settings": {
"authorisation": [
{
"id": "api_key",
"value": "your_external_api_key"
}
]
},
"iat": 1716214515,
"exp": 1716218115
}
lead_created
, deal_updated
)const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).send('Missing or invalid authorization header');
}
const token = authHeader.split('Bearer ')[1];
try {
// Verify JWT token using your integration UUID as the secret
const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);
console.log('Webhook context:', {
company: decoded.company_uuid,
user: decoded.user_uuid,
event: decoded.event_id,
apiKey: decoded.api_key
});
// Process webhook payload
const { lead, configuration } = req.body;
if (lead) {
console.log(`Processing lead: ${lead.company_name}`);
await processLead(lead, configuration, decoded);
}
res.status(200).send('OK');
} catch (error) {
console.error('JWT verification failed:', error.message);
return res.status(401).send('Invalid token');
}
});
async function processLead(lead, configuration, context) {
// Use context for company-specific processing
console.log(`Processing lead for company: ${context.company_uuid}`);
// Access configuration settings
const settings = configuration.find(c => c.id === 'lead_integration_settings');
const uniqueField = settings?.selected?.find(s => s.id === 'unique_field_lead');
console.log(`Unique field mapping: ${uniqueField?.value}`);
// Your integration logic here
}
app.listen(3000);
from flask import Flask, request, abort, jsonify
import jwt
import os
import json
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
abort(401, 'Missing or invalid authorization header')
token = auth_header.split('Bearer ')[1]
try:
# Verify JWT token using your integration UUID as the secret
decoded = jwt.decode(token, os.getenv('INTEGRATION_UUID'), algorithms=['HS256'])
print('Webhook context:', {
'company': decoded['company_uuid'],
'user': decoded['user_uuid'],
'event': decoded['event_id'],
'api_key': decoded.get('api_key')
})
# Process webhook payload
payload = request.json
if 'lead' in payload:
print(f"Processing lead: {payload['lead']['company_name']}")
process_lead(payload['lead'], payload['configuration'], decoded)
elif 'deal' in payload:
print(f"Processing deal: {payload['deal']['label']}")
process_deal(payload['deal'], payload['configuration'], decoded)
elif 'partnership' in payload:
print(f"Processing partnership: {payload['partnership']['company_name']}")
process_partnership(payload['partnership'], payload['configuration'], decoded)
return 'OK', 200
except jwt.InvalidTokenError as e:
print(f'JWT verification failed: {e}')
abort(401, 'Invalid token')
def process_lead(lead, configuration, context):
# Use context for company-specific processing
print(f"Processing lead for company: {context['company_uuid']}")
# Access configuration settings
settings = next((c for c in configuration if c['id'] == 'lead_integration_settings'), None)
if settings:
unique_field = next((s for s in settings['selected'] if s['id'] == 'unique_field_lead'), None)
if unique_field:
print(f"Unique field mapping: {unique_field['value']}")
# Your integration logic here
pass
def process_deal(deal, configuration, context):
# Deal processing logic
pass
def process_partnership(partnership, configuration, context):
# Partnership processing logic
pass
if __name__ == '__main__':
app.run(debug=True)
<?php
// webhook.php
require_once 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function handleWebhook() {
// Get authorization header
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
http_response_code(401);
exit('Missing or invalid authorization header');
}
$token = substr($authHeader, 7); // Remove 'Bearer ' prefix
try {
// Verify JWT token using your integration UUID as the secret
$decoded = JWT::decode($token, new Key($_ENV['INTEGRATION_UUID'], 'HS256'));
error_log('Webhook context: ' . json_encode([
'company' => $decoded->company_uuid,
'user' => $decoded->user_uuid,
'event' => $decoded->event_id,
'api_key' => $decoded->api_key ?? null
]));
// Get and process webhook payload
$payload = json_decode(file_get_contents('php://input'), true);
if (isset($payload['lead'])) {
error_log("Processing lead: " . $payload['lead']['company_name']);
processLead($payload['lead'], $payload['configuration'], $decoded);
} elseif (isset($payload['deal'])) {
error_log("Processing deal: " . $payload['deal']['label']);
processDeal($payload['deal'], $payload['configuration'], $decoded);
} elseif (isset($payload['partnership'])) {
error_log("Processing partnership: " . $payload['partnership']['company_name']);
processPartnership($payload['partnership'], $payload['configuration'], $decoded);
}
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
error_log('JWT verification failed: ' . $e->getMessage());
http_response_code(401);
exit('Invalid token');
}
}
function processLead($lead, $configuration, $context) {
// Use context for company-specific processing
error_log("Processing lead for company: " . $context->company_uuid);
// Access configuration settings
foreach ($configuration as $config) {
if ($config['id'] === 'lead_integration_settings') {
foreach ($config['selected'] as $setting) {
if ($setting['id'] === 'unique_field_lead') {
error_log("Unique field mapping: " . $setting['value']);
}
}
}
}
// Your integration logic here
}
function processDeal($deal, $configuration, $context) {
// Deal processing logic
}
function processPartnership($partnership, $configuration, $context) {
// Partnership processing logic
}
handleWebhook();
?>
// ❌ DON'T: Process webhooks without verification
app.post("/webhook", (req, res) => {
const payload = req.body;
processEvent(payload); // Dangerous!
res.send("OK");
});
// ✅ DO: Always verify JWT tokens first
app.post("/webhook", (req, res) => {
const token = extractBearerToken(req);
try {
const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);
processEvent(req.body, decoded);
res.send("OK");
} catch (error) {
return res.status(401).send("Invalid token");
}
});
function verifyToken(token, integrationUuid) {
try {
const decoded = jwt.verify(token, integrationUuid);
// JWT library automatically checks expiration,
// but you can add custom validation
if (decoded.exp < Math.floor(Date.now() / 1000)) {
throw new Error("Token expired");
}
return decoded;
} catch (error) {
console.error("Token verification failed:", error.message);
throw error;
}
}
function validateWebhookContext(decoded, expectedCompany = null) {
// Check required fields
if (!decoded.company_uuid || !decoded.user_uuid || !decoded.event_id) {
throw new Error("Missing required token fields");
}
// Optionally validate company
if (expectedCompany && decoded.company_uuid !== expectedCompany) {
throw new Error("Company mismatch");
}
// Validate event types
const validEventTypes = [
"partner_created",
"partner_updated",
"partner_contact_created",
"lead_created",
"lead_updated",
"lead_deleted",
"deal_created",
"deal_updated",
"deal_deleted",
"lead_note_created",
"deal_note_created",
];
if (!validEventTypes.includes(decoded.event_id)) {
throw new Error(`Invalid event type: ${decoded.event_id}`);
}
return true;
}
# Environment variables
INTEGRATION_UUID=your_integration_uuid_from_journeybee
# Docker secrets
docker secret create integration-uuid /path/to/uuid/file
# Kubernetes secrets
kubectl create secret generic webhook-secret --from-literal=integration_uuid=your_uuid
const processedEvents = new Set();
app.post("/webhook", async (req, res) => {
const token = extractBearerToken(req);
const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);
// Check for duplicate events
const eventKey = `${decoded.company_uuid}:${decoded.event_id}:${
req.body.lead?.uuid || req.body.deal?.uuid || req.body.partnership?.uuid
}`;
if (processedEvents.has(eventKey)) {
console.log(`Duplicate event ignored: ${eventKey}`);
return res.status(200).send("Already processed");
}
// Process event
await processEvent(req.body, decoded);
// Mark as processed
processedEvents.add(eventKey);
res.status(200).send("OK");
});
function validateWebhookPayload(payload, eventType) {
// Validate based on event type
switch (eventType) {
case "lead_created":
case "lead_updated":
if (!payload.lead || !payload.lead.uuid || !payload.lead.company_name) {
throw new Error("Invalid lead payload structure");
}
break;
case "deal_created":
case "deal_updated":
if (!payload.deal || !payload.deal.uuid || !payload.deal.label) {
throw new Error("Invalid deal payload structure");
}
break;
case "partner_created":
case "partner_updated":
if (
!payload.partnership ||
!payload.partnership.uuid ||
!payload.partnership.company_name
) {
throw new Error("Invalid partnership payload structure");
}
break;
default:
throw new Error(`Unknown event type: ${eventType}`);
}
// Validate configuration array
if (!Array.isArray(payload.configuration)) {
throw new Error("Configuration must be an array");
}
return true;
}
const rateLimit = require("express-rate-limit");
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limit each IP to 1000 requests per windowMs
message: "Too many webhook requests",
standardHeaders: true,
legacyHeaders: false,
});
app.use("/webhook", webhookLimiter);
app.post("/webhook", (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
console.warn("Webhook security: Missing authorization header");
return res.status(401).send("Missing authorization");
}
try {
const token = authHeader.split("Bearer ")[1];
const decoded = jwt.verify(token, process.env.INTEGRATION_UUID);
console.info(
`Webhook security: Valid request from company ${decoded.company_uuid} for event ${decoded.event_id}`
);
// Process webhook...
} catch (error) {
console.error(
`Webhook security: Token verification failed - ${error.message}`
);
return res.status(401).send("Invalid token");
}
});
# Generate a test JWT token (replace with your integration UUID)
JWT_TOKEN="your-test-jwt-token"
curl -X POST https://your-webhook-url.com/webhook \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{
"lead": {
"uuid": "lead_test_123",
"company_name": "Test Company",
"email": "test@company.com"
},
"configuration": []
}'
const jwt = require("jsonwebtoken");
const testToken = jwt.sign(
{
company_uuid: "company_test_123",
user_uuid: "user_test_123",
event_id: "lead_created",
api_key: "test_api_key",
external_settings: {
authorisation: [],
},
},
"your-integration-uuid"
);
console.log("Test JWT token:", testToken);