← Volver al catálogo
WebReferenciaIntermedioGratis

FusionAuth Webhooks — Recepción y Verificación JWT

Referencia de código para recibir y verificar webhooks de FusionAuth usando firma JWT (HMAC). Incluye handlers listos para Express y FastAPI con verificación de integridad del cuerpo de la petición.

Descargar skill (.zip)

Descarga abierta · sin registro · para Node.js, Express, Python

// resultado_de_ejemplo

// Generated with: fusionauth-webhooks skill // https://github.com/hookdeck/webhook-skills // Cliente: NutriFlow SaaS — Webhooks de autenticación FusionAuth // Skill: fusionauth-webhooks-verificacion (CULTIVA IA / Web)

'use strict';

const express = require('express'); const crypto = require('crypto'); const jose = require('jose');

// ─── Configuración ───────────────────────────────────────────────────────────

const PORT = process.env.PORT || 3001; const SECRET = process.env.FUSIONAUTH_WEBHOOK_SECRET; // HMAC key de Key Master

const app = express();

// ─── Utilidad: logger JSON estructurado (compatible Datadog) ─────────────────

function log(level, message, extra = {}) { const entry = { timestamp: new Date().toISOString(), level, service: 'nutriflow-webhooks', message, ...extra, }; // eslint-disable-next-line no-console console.log(JSON.stringify(entry)); }

// ─── Verificación JWT/HMAC de FusionAuth ─────────────────────────────────────

/**

  • Verifica la autenticidad del webhook de FusionAuth.
  • FusionAuth firma cada petición con un JWT (header X-FusionAuth-Signature-JWT)
  • que contiene el claim request_body_sha256 con el hash SHA-256 del body
  • codificado en base64. Si el JWT está bien firmado Y el hash coincide,
  • el evento es auténtico.
  • @param {Buffer} rawBody - Body sin parsear (express.raw)
  • @param {string} signatureJwt - Valor del header X-FusionAuth-Signature-JWT
  • @param {string} hmacSecret - Clave HMAC del Key Master de FusionAuth
  • @returns {Promise} */ async function verifyFusionAuthWebhook(rawBody, signatureJwt, hmacSecret) { if (!signatureJwt || !hmacSecret) return false;

try { // 1. Verificar firma del JWT con la clave HMAC const key = new TextEncoder().encode(hmacSecret); const { payload } = await jose.jwtVerify(signatureJwt, key, { algorithms: ['HS256', 'HS384', 'HS512'], });

// 2. Calcular SHA-256 del body raw y comparar con el claim
const bodyHash = crypto
  .createHash('sha256')
  .update(rawBody)
  .digest('base64');

return payload.request_body_sha256 === bodyHash;

} catch (err) { log('error', 'JWT verification failed', { error: err.message }); return false; } }

// ─── Handlers de eventos ─────────────────────────────────────────────────────

/**

  • user.create — Nuevo usuario registrado en FusionAuth.
  • Acción: crear perfil en BD interna + disparar email de bienvenida. */ async function handleUserCreate(event) { const { id, email, fullName } = event.user ?? {}; log('info', 'user.create — creando perfil en NutriFlow', { userId: id, email });

// TODO (producción): await db.users.create({ fusionAuthId: id, email, fullName }); // TODO (producción): await mailer.sendWelcome({ to: email, name: fullName });

log('info', 'user.create — perfil creado y email de bienvenida encolado', { userId: id }); }

/**

  • user.login.success — Login correcto.
  • Acción: actualizar lastSeenAt y contador de sesiones. */ async function handleLoginSuccess(event) { const userId = event.user?.id; log('info', 'user.login.success — actualizando último acceso', { userId });

// TODO (producción): await db.users.update(userId, { // lastSeenAt: new Date(event.createInstant), // $increment: { sessionCount: 1 }, // }); }

/**

  • user.registration.create — Usuario asignado a una aplicación (clínica).
  • Acción: asociar al usuario con el tenant correcto en NutriFlow. */ async function handleRegistrationCreate(event) { const { id: userId } = event.user ?? {}; const { applicationId } = event.registration ?? {}; log('info', 'user.registration.create — asignando clínica', { userId, applicationId });

// TODO (producción): await db.clinicMembers.upsert({ userId, clinicId: applicationId }); }

/**

  • user.email.verified — Email verificado.
  • Acción: desbloquear funcionalidades premium. */ async function handleEmailVerified(event) { const userId = event.user?.id; log('info', 'user.email.verified — desbloqueando premium', { userId });

// TODO (producción): await db.users.update(userId, { emailVerified: true, tier: 'premium' }); }

/**

  • user.deactivate — Cuenta desactivada en FusionAuth.
  • Acción: revocar acceso sin borrar datos (soft-disable). */ async function handleUserDeactivate(event) { const userId = event.user?.id; log('warn', 'user.deactivate — revocando acceso', { userId });

// TODO (producción): await db.users.update(userId, { active: false, revokedAt: new Date() }); // TODO (producción): await sessions.revokeAll(userId); }

// ─── Dispatcher ──────────────────────────────────────────────────────────────

const EVENT_HANDLERS = { 'user.create': handleUserCreate, 'user.login.success': handleLoginSuccess, 'user.registration.create': handleRegistrationCreate, 'user.email.verified': handleEmailVerified, 'user.deactivate': handleUserDeactivate, };

async function dispatchEvent(eventType, eventPayload) { const handler = EVENT_HANDLERS[eventType]; if (!handler) { log('warn', 'Evento no manejado — ignorando', { eventType }); return; } await handler(eventPayload); }

// ─── Endpoint principal ──────────────────────────────────────────────────────

/**

  • POST /webhooks/fusionauth

  • IMPORTANTE: express.raw() es obligatorio. Si se usa express.json() el body

  • se re-serializa y el hash SHA-256 ya no coincide con el original. */ app.post( '/webhooks/fusionauth', express.raw({ type: 'application/json' }), async (req, res) => { const startTime = Date.now(); const signatureJwt = req.headers['x-fusionauth-signature-jwt'];

    // 1. Verificar firma HMAC-JWT const isValid = await verifyFusionAuthWebhook(req.body, signatureJwt, SECRET); if (!isValid) { log('error', 'Firma inválida — rechazando webhook', { ip: req.ip, hasHeader: !!signatureJwt, }); return res.status(401).json({ error: 'Invalid signature' }); }

    // 2. Parsear payload (sólo después de verificar) let event; try { event = JSON.parse(req.body.toString()); } catch (parseErr) { log('error', 'Body no es JSON válido', { error: parseErr.message }); return res.status(400).json({ error: 'Invalid JSON body' }); }

    const eventType = event?.event?.type ?? 'unknown'; log('info', 'Webhook recibido', { eventType, eventId: event?.event?.id });

    // 3. Despachar handler (sin bloquear la respuesta 2xx) // FusionAuth necesita un 200 rápido; procesamos en background. res.json({ received: true });

    try { await dispatchEvent(eventType, event.event); log('info', 'Evento procesado', { eventType, durationMs: Date.now() - startTime, }); } catch (handlerErr) { log('error', 'Error en handler de evento', { eventType, error: handlerErr.message, }); // No relanzamos — la respuesta 200 ya fue enviada y no podemos cambiarla. // En producción: enviar a dead-letter queue o alertar en Datadog. } } );

// ─── Health check ─────────────────────────────────────────────────────────────

app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'nutriflow-webhooks' }));

// ─── Arranque ─────────────────────────────────────────────────────────────────

app.listen(PORT, () => { log('info', NutriFlow Webhooks escuchando en :${PORT}, { endpoint: http://localhost:${PORT}/webhooks/fusionauth, }); });

module.exports = app; // exportado para tests

// qué_hace

Proporciona código listo para usar que recibe eventos de autenticación de FusionAuth (user.create, user.login, etc.) y verifica su autenticidad mediante JWT firmado con HMAC.

// cómo_lo_hace

Extrae el JWT del header X-FusionAuth-Signature-JWT, lo verifica con la clave HMAC secreta, compara el hash SHA-256 del cuerpo y despacha el evento al handler correspondiente.

// ejemplo_de_uso

Necesaria cuando FusionAuth envía eventos de autenticación y tu backend debe procesarlos de forma segura. Ej.: recibe el webhook de user.login, verifica el JWT y registra el acceso en tu base de datos.

// plataformas

Node.jsExpressPythonFastAPIFusionAuthHookdeck
Categoría
Web
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// opiniones_de_la_comunidad

Opiniones

Cargando opiniones…

// pase_cultiva_ia

Llévate todo el arsenal con el Pase

Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.

Pago único · IVA incluido · pago seguro con Stripe.

Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.