Auth Webhook — Node.js
Este conteúdo não está disponível em sua língua ainda.
A complete Node.js Lambda template that handles all three channels (WebChat, WhatsApp, Instagram) with challenge-response support and HMAC signature verification.
Full example
Section titled “Full example”const crypto = require('crypto');
// Replace with your actual database and cache clientsconst db = require('./db'); // Your user databaseconst cache = require('./cache'); // Redis, DynamoDB, or similar (for challenge TTL)
const AUTH_SECRET = process.env.AUTH_SECRET; // Your webhook secret from SynapsAI
exports.handler = async (event) => { const body = JSON.parse(event.body); const { credentials, challengeResponse, context } = body;
// 1. Verify HMAC signature (if secret is configured) if (AUTH_SECRET) { const signature = event.headers['x-synapsai-signature']; if (!verifySignature(body, signature, AUTH_SECRET)) { return response(401, { verified: false, error: 'Invalid signature' }); } }
// 2. Handle challenge answer (user is responding to a previous challenge) if (challengeResponse) { return handleChallengeResponse(challengeResponse, context); }
// 3. Route by channel switch (context.channel) { case 'CHAT': return handleWebChat(credentials, context); case 'WHATSAPP': case 'INSTAGRAM': return handleMessaging(credentials, context); default: return response(200, { verified: false, error: 'Unsupported channel' }); }};
// --- WebChat: email + password login ---async function handleWebChat(credentials, context) { const user = await db.findByEmail(credentials.email);
if (!user || !await bcryptCompare(credentials.password, user.passwordHash)) { return response(200, { verified: false, error: 'Invalid email or password', retriesLeft: 2, }); }
return response(200, { verified: true, userId: user.id, identity: { name: user.name, email: user.email }, });}
// --- WhatsApp / Instagram: document ID + phone challenge ---async function handleMessaging(credentials, context) { const customer = await db.findByDocumentId(credentials.documentId);
if (!customer) { return response(200, { verified: false, error: 'Document not found', retriesLeft: 2, }); }
// Auto-verify if WhatsApp phone matches a registered phone const whatsAppPhone = context.implicitIdentity?.phone; if (whatsAppPhone && customer.phones.includes(normalizePhone(whatsAppPhone))) { return response(200, { verified: true, userId: customer.id, identity: { name: customer.name, email: customer.email }, }); }
// Otherwise: challenge with masked phones const challengeId = `ch_${crypto.randomUUID().slice(0, 8)}`; const maskedPhones = customer.phones.map((p) => ({ label: maskPhone(p), value: p, }));
// Store challenge with 5-minute TTL await cache.set( challengeId, { userId: customer.id, phones: customer.phones, identity: { name: customer.name, email: customer.email }, }, { EX: 300 }, );
return response(200, { verified: false, challenge: { challengeId, type: 'SELECT_OPTION', prompt: 'Confirm one of your registered phones:', options: maskedPhones, hint: 'Type the full number with area code', }, });}
// --- Challenge response handler ---async function handleChallengeResponse(challengeResponse, context) { const challenge = await cache.get(challengeResponse.challengeId);
// Challenge expired (TTL exceeded) if (!challenge) { return response(200, { verified: false, error: 'Challenge expired. Please start verification again.', retriesLeft: 0, }); }
// Check answer if (challenge.phones.includes(normalizePhone(challengeResponse.answer))) { await cache.del(challengeResponse.challengeId); // Clean up return response(200, { verified: true, userId: challenge.userId, identity: challenge.identity, }); }
return response(200, { verified: false, error: 'Incorrect phone number', retriesLeft: 1, });}
// --- Helpers ---
function verifySignature(body, signature, secret) { if (!signature) return false; const expected = crypto .createHmac('sha256', secret) .update(JSON.stringify(body)) .digest('hex'); return signature === `sha256=${expected}`;}
function normalizePhone(phone) { return phone.replace(/[^0-9+]/g, '');}
function maskPhone(phone) { // "+5551981975196" → "555198xxxxx96" const digits = phone.replace(/\D/g, ''); if (digits.length < 8) return digits; return digits.slice(0, 6) + 'x'.repeat(digits.length - 8) + digits.slice(-2);}
async function bcryptCompare(plain, hash) { const bcrypt = require('bcryptjs'); return bcrypt.compare(plain, hash);}
function response(statusCode, body) { return { statusCode, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), };}Key points
Section titled “Key points”- Signature verification comes first — reject unauthorized requests before processing credentials.
- Channel routing uses
context.channelto apply the right verification logic. - Auto-verify on WhatsApp when
implicitIdentity.phonematches a registered phone — no challenge needed. - Challenge TTL is your responsibility. Use Redis
EXor DynamoDB TTL (recommended: 5 minutes). - Never log credentials, document IDs, full phone numbers, or webhook response bodies.
See also
Section titled “See also”- Auth Webhook Contract — Full request/response reference
- Python webhook starter — Same example in Python