Aller au contenu

Auth Webhook — Node.js

Ce contenu n’est pas encore disponible dans votre langue.

A complete Node.js Lambda template that handles all three channels (WebChat, WhatsApp, Instagram) with challenge-response support and HMAC signature verification.

const crypto = require('crypto');
// Replace with your actual database and cache clients
const db = require('./db'); // Your user database
const 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),
};
}
  • Signature verification comes first — reject unauthorized requests before processing credentials.
  • Channel routing uses context.channel to apply the right verification logic.
  • Auto-verify on WhatsApp when implicitIdentity.phone matches a registered phone — no challenge needed.
  • Challenge TTL is your responsibility. Use Redis EX or DynamoDB TTL (recommended: 5 minutes).
  • Never log credentials, document IDs, full phone numbers, or webhook response bodies.