Escribir Actions Personalizadas
Las Actions son funciones JavaScript personalizadas que se ejecutan en puntos específicos del pipeline de autenticación de Auris. Permiten extender el comportamiento de la plataforma sin bifurcar ni modificar Auris en sí — añade claims personalizados a los tokens, bloquea inicios de sesión basándose en datos externos, llama a webhooks, aplica reglas de negocio o integra con servicios de terceros.
Cuándo usar Actions frente a otros puntos de extensión
| Mecanismo | Ideal para |
|---|---|
| Actions | Lógica que debe ejecutarse de forma síncrona durante el flujo de autenticación (bloquear un inicio de sesión, añadir claims, modificar metadatos de usuario) |
| Custom Claims | Enriquecimiento de tokens estático o basado en atributos que no requiere llamadas externas o lógica condicional |
| Webhooks | Notificaciones asíncronas después de que ocurran eventos (registro de auditoría, analíticas, alertas en Slack) |
Si tu caso de uso requiere una llamada a una API externa que debe completarse con éxito antes de que se complete el inicio de sesión, usa una Action. Si solo necesitas notificar a un sistema después del hecho, usa un Webhook.
El Entorno Sandbox
Las Actions se ejecutan en un sandbox de JavaScript restringido. El sandbox proporciona:
APIs Disponibles
| API | Notas |
|---|---|
fetch() | Realizar solicitudes HTTP a servicios externos. API Fetch completa. |
JSON | JSON.parse() y JSON.stringify() |
Date | Construcción y manipulación de fechas |
Math | Operaciones matemáticas |
console.log() | La salida se captura y es visible en los logs de la action |
String, Number, Boolean, Array, Object | Tipos integrados estándar |
Promise, async/await | El código asíncrono es completamente compatible |
URL, URLSearchParams | Análisis y construcción de URLs |
TextEncoder, TextDecoder | Utilidades de codificación de texto |
crypto.randomUUID() | Generación de UUIDs |
APIs Bloqueadas
Las siguientes están bloqueadas por seguridad:
| Bloqueado | Motivo |
|---|---|
require(), import | Sin acceso al sistema de módulos |
process | Sin acceso a variables de entorno ni información del proceso |
| Funciones de ejecución de código dinámico | Sin generación de código en tiempo de ejecución |
global, globalThis | Sin acceso al ámbito global |
fs, child_process | Sin sistema de archivos ni creación de procesos |
Las Actions tienen un tiempo de espera de ejecución configurable (por defecto: 5 segundos, máximo: 30 segundos). Si una action supera el tiempo de espera, se termina y se aplica el comportamiento de fallo configurado (permitir o denegar la solicitud).
Tipos de Disparadores
Las Actions se activan en puntos específicos del flujo de autenticación. Cada disparador recibe un objeto context diferente:
| Disparador | Cuándo se activa | Usos comunes |
|---|---|---|
pre-login | Antes de que se verifiquen las credenciales | Bloquear inicios de sesión por dominio de email, comprobar listas de bloqueo externas |
post-login | Tras una autenticación exitosa, antes de la emisión de tokens | Añadir claims personalizados, sincronizar con sistemas externos, MFA condicional |
pre-signup | Antes de que se cree un nuevo usuario | Validar dominio de email, comprobar requisitos de invitación |
post-signup | Después de que se crea un nuevo usuario | Enviar webhook de bienvenida, asignar a organización, establecer metadatos |
post-change-password | Después de un cambio de contraseña | Notificar a sistemas externos, invalidar sesiones en caché |
pre-m2m-token | Antes de que se emita un token M2M | Restringir scopes, validar el cliente contra reglas externas |
El Objeto Context
Cada action recibe un objeto context con información sobre la solicitud actual. La forma varía según el disparador:
pre-login y post-login
{
user: {
id: 'user-123',
email: '[email protected]',
username: 'alice',
firstName: 'Alice',
lastName: 'Smith',
roles: ['editor', 'viewer'],
metadata: { plan: 'pro', company: 'Acme' },
},
request: {
ip: '203.0.113.42',
userAgent: 'Mozilla/5.0...',
geoip: { country: 'IT', city: 'Milan' },
},
application: {
id: 'app-456',
name: 'Dashboard',
type: 'WEB',
},
tenant: {
id: 'tenant-789',
name: 'acme-corp',
},
}pre-m2m-token
{
client: {
id: 'client-abc',
name: 'Billing Service',
type: 'M2M',
},
requestedScopes: ['read:users', 'manage:billing'],
tenant: { id: 'tenant-789', name: 'acme-corp' },
}El Tipo de Retorno ActionResult
Las Actions devuelven un objeto que controla el resultado:
interface ActionResult {
allow: boolean // true = proceder, false = bloquear la solicitud
message?: string // Mensaje de error mostrado al usuario cuando allow=false
claims?: Record<string, any> // Claims personalizados para añadir al token (post-login, pre-m2m-token)
metadata?: Record<string, any> // Metadatos para establecer en el registro del usuario
}Si una action no devuelve un resultado (o devuelve undefined), la solicitud procede con normalidad.
Ejemplos Prácticos
Bloquear inicios de sesión por dominio de email
// Disparador: pre-login
// Bloquear proveedores de email personal para que no inicien sesión
const blockedDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com']
const domain = context.user.email.split('@')[1]
if (blockedDomains.includes(domain)) {
return {
allow: false,
message: 'Las direcciones de email personales no están permitidas. Por favor, usa tu email corporativo.',
}
}
return { allow: true }Añadir claims personalizados basados en roles
// Disparador: post-login
// Añadir un claim 'plan' y banderas de funcionalidades basadas en metadatos de usuario
const plan = context.user.metadata?.plan || 'free'
const featureFlags = {
free: { maxProjects: 3, analytics: false, exportEnabled: false },
pro: { maxProjects: 50, analytics: true, exportEnabled: true },
enterprise: { maxProjects: -1, analytics: true, exportEnabled: true },
}
return {
allow: true,
claims: {
plan,
features: featureFlags[plan] || featureFlags.free,
'https://myapp.com/roles': context.user.roles,
},
}Registrar eventos de autenticación en un webhook externo
// Disparador: post-login
// Enviar una notificación webhook en cada inicio de sesión
try {
await fetch('https://hooks.example.com/auth-events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'user.login',
userId: context.user.id,
email: context.user.email,
ip: context.request.ip,
country: context.request.geoip?.country,
timestamp: new Date().toISOString(),
}),
})
} catch (err) {
// No crítico -- registrar pero no bloquear el inicio de sesión
console.log('Webhook fallido:', err.message)
}
return { allow: true }Restringir scopes M2M basándose en políticas externas
// Disparador: pre-m2m-token
// Consultar un servicio de políticas externo antes de emitir tokens M2M
const response = await fetch('https://policy.internal/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: context.client.id,
scopes: context.requestedScopes,
}),
})
if (!response.ok) {
return {
allow: false,
message: 'Solicitud de token denegada por el servicio de políticas.',
}
}
const policy = await response.json()
return {
allow: true,
claims: {
scope: policy.allowedScopes.join(' '),
'policy-version': policy.version,
},
}Disparador MFA condicional
// Disparador: post-login
// Requerir MFA para roles de administrador o inicios de sesión desde nuevos países
const isAdmin = context.user.roles.includes('admin')
const isNewCountry = context.request.geoip?.country !== context.user.metadata?.lastCountry
if (isAdmin || isNewCountry) {
return {
allow: true,
metadata: {
lastCountry: context.request.geoip?.country,
requireMfa: true,
},
}
}
return {
allow: true,
metadata: { lastCountry: context.request.geoip?.country },
}Orden de Ejecución
Cuando se configuran múltiples actions para el mismo disparador, se ejecutan en el orden definido por su campo order (ascendente). Cada action recibe el mismo contexto original — la salida de una action no modifica el contexto para las acciones posteriores.
Sin embargo, los claims y metadatos de todas las actions se fusionan. Si dos actions establecen la misma clave de claim, la última action en el orden gana.
| Orden de acción | Nombre de la acción | Salida de claims |
|---|---|---|
| 1 | Añadir claim de plan | { plan: 'pro' } |
| 2 | Añadir banderas de funcionalidades | { features: { ... } } |
| 3 | Añadir claim de región | { region: 'eu' } |
Claims finales del token: { plan: 'pro', features: { ... }, region: 'eu' }
Si alguna action devuelve allow: false, el pipeline completo se detiene y la solicitud se deniega, independientemente de las acciones posteriores.
El Editor Visual Blueprint
Para equipos que prefieren un enfoque visual, Auris ofrece un editor Blueprint — un constructor de reglas visuales basado en nodos inspirado en el sistema Blueprint de Unreal Engine.
El editor Blueprint representa las actions como un grafo de nodos conectados:
| Tipo de nodo | Color | Propósito |
|---|---|---|
| Disparador | Naranja | El punto de entrada — qué evento de autenticación activa la regla |
| Condición | Cian | Comprobar un campo contra un valor (p. ej., user.email contiene @acme.com) |
| Puerta lógica | Violeta | Combinar condiciones con AND/OR |
| Denegar | Rosa | Bloquear la solicitud con un mensaje de error |
| Establecer claims | Azul | Añadir pares clave-valor al token |
| Establecer metadatos | Esmeralda | Actualizar metadatos del usuario |
| Log | Verde | Escribir en los logs de la action |
El editor Blueprint y el editor de código son dos vistas de la misma action subyacente. Los cambios en uno se sincronizan con el otro. Puedes empezar con Blueprint para reglas simples y cambiar al código para lógica compleja.
Para acceder al editor Blueprint, abre la página de detalle de una action en la Consola y haz clic en la pestaña Blueprint.
Depuración de Actions
Logs de Actions
Cada ejecución de una action queda registrada. Ver los logs en Consola -> Actions -> [Nombre de la action] -> pestaña Logs. Cada entrada del log incluye:
- Marca de tiempo de ejecución
- Evento disparador
- Duración de ejecución (ms)
- Estado (éxito, error, timeout)
- Salida de console.log
- Valor de retorno
Usar console.log
La salida de console.log() se captura y almacena en el log de la action. Úsalo para depurar:
console.log('Roles del usuario:', JSON.stringify(context.user.roles))
console.log('IP de la solicitud:', context.request.ip)
console.log('Datos GeoIP:', JSON.stringify(context.request.geoip))
// Esta salida aparece en la pestaña Logs de la action
return { allow: true }Manejo de Errores
Si una action lanza un error no manejado, el comportamiento depende del ajuste de modo de fallo de la action:
| Modo de fallo | Comportamiento |
|---|---|
allow (por defecto) | La solicitud procede. El error se registra. |
deny | La solicitud se bloquea con un mensaje de error genérico. |
Usa siempre try/catch alrededor de las llamadas a APIs externas para evitar que los fallos bloqueen los inicios de sesión:
try {
const result = await fetch('https://external-api.example.com/check')
// procesar resultado
} catch (err) {
console.log('API externa fallida, procediendo:', err.message)
// No bloquear el inicio de sesión porque un servicio externo esté caído
}
return { allow: true }Consejos de Rendimiento
- Mantén las actions rápidas — Apunta a menos de 100ms de tiempo de ejecución. Cada milisegundo añade latencia al inicio de sesión.
- Usa try/catch para llamadas externas — Nunca dejes que el fallo de una API externa no crítica bloquee un inicio de sesión.
- Evita llamadas externas secuenciales — Si necesitas múltiples llamadas a la API, usa
Promise.all()para ejecutarlas en paralelo. - Caché de búsquedas costosas — Para datos que cambian poco frecuentemente, considera almacenar en caché en los metadatos del usuario y actualizar periódicamente en lugar de consultar en cada inicio de sesión.
- Establece timeouts apropiados — Usa
AbortControllerconfetch()para establecer timeouts explícitos más cortos que el timeout de la action:
const controller = new AbortController()
setTimeout(() => controller.abort(), 3000) // timeout de 3 segundos
const response = await fetch('https://slow-api.example.com/check', {
signal: controller.signal,
})Endpoints de API
/api/actionsRequires: view:actionsLista todas las actions para el tenant. Soporta filtrado por tipo de disparador y estado.
/api/actionsRequires: manage:actionsCrea una nueva action. Requiere name, trigger, code y opcionales order, timeout, status.
/api/actions/:idRequires: view:actionsObtiene los detalles de una action incluyendo código, disparador, recuento de ejecuciones y recuento de errores.
/api/actions/:idRequires: manage:actionsActualiza el código, disparador, orden, timeout o estado de una action.
/api/actions/:idRequires: manage:actionsElimina una action.
/api/actions/:id/logsRequires: view:actionsLista los logs de ejecución de una action específica. Incluye duración, estado, salida de consola y valor de retorno.
Permisos Requeridos
| Operación | Permiso |
|---|---|
| Ver actions | view:actions |
| Crear, editar, eliminar actions | manage:actions |
| Ver logs de actions | view:actions |
Guías Relacionadas
- Claims JWT Personalizados — Enriquecimiento declarativo de claims (sin código requerido)
- Configurar Webhooks — Notificaciones de eventos asíncronas
- Roles y Permisos — Gestión de los permisos referenciados en las actions