Actions & Esecuzione in Sandbox
Il Problema: Estensibilità Senza Fork
Ogni piattaforma di identità affronta alla fine la stessa tensione: i clienti hanno bisogno di logica personalizzata nei loro flussi di autenticazione, ma la piattaforma non può anticipare ogni requisito.
Senza un meccanismo di estensibilità, le opzioni sono: feature request (lente), webhook a servizi esterni (aggiunge latenza, richiede al cliente di ospitare un server webhook), fork della piattaforma (incubo di manutenzione), o regole solo configurazione (troppo limitate).
Ciò di cui i clienti hanno effettivamente bisogno è la capacità di eseguire codice personalizzato in punti specifici del flusso di autenticazione — prima del login (per bloccare tentativi sospetti), dopo il login (per arricchire i token con claim personalizzati), dopo la registrazione (per innescare workflow di onboarding), e altro ancora.
Come Funziona la Sandbox di Auris
Le Actions di Auris eseguono JavaScript personalizzato all’interno di un ambiente controllato usando il costruttore Function di JavaScript. Questo meccanismo crea una funzione da una stringa di codice con uno scope esplicito — il codice dell’action vede solo le variabili che Auris fornisce esplicitamente.
Pipeline di Esecuzione
Passo 1: Analisi Statica
Prima di eseguire qualsiasi codice, Auris scansiona il sorgente dell’action per pattern bloccati:
const BLOCKED_PATTERNS = [
'require', // Nessun caricamento di moduli
'import', // Nessun import ES module
'process', // Nessun accesso a process.env, process.exit, ecc.
'global', // Nessun accesso all'oggetto global
'child_process', // Nessun avvio di sottoprocessi
'fs', // Nessun accesso al filesystem
]L’analisi statica non è di per sé un confine di sicurezza. Un attaccante determinato potrebbe offuscare le parole chiave bloccate. L’analisi statica esiste per intercettare l’uso accidentale e fornire messaggi di errore chiari. La vera sicurezza viene dallo scope ristretto dell’oggetto fornito alla funzione costruita.
Passo 2: Costruzione della Funzione
Il codice dell’action viene usato per costruire una funzione con context come unico parametro. La funzione risultante si comporta come:
// Concettualmente equivalente a:
function anonymous(context) {
// Il codice dell'action viene eseguito qui
// Può accedere SOLO a:
// - 'context' (l'argomento)
// - JavaScript built-in (Array, Object, String, Math, Date, JSON, ecc.)
// - fetch (fornito esplicitamente per HTTP outbound)
// - console (catturato, non stdout reale)
}La proprietà critica: la funzione non ha closure sulle variabili del server. Non può accedere a require, process, connessioni al database, o qualsiasi altro stato lato server.
Passo 3: L’Oggetto Context
Il parametro context fornisce all’action informazioni sull’evento di autenticazione corrente:
interface ActionContext {
user: {
id: string
email: string
username: string
roles: string[]
metadata: Record<string, unknown>
}
request: {
ip: string
userAgent: string
geoip?: { country: string; city: string }
}
tenant: { id: string; name: string }
application: { id: string; clientId: string; name: string }
trigger: string // 'pre_login' | 'post_login' | 'pre_signup' | 'post_signup' | ...
fetch: typeof fetch // HTTP outbound
console: { // Logging catturato (va su ActionLog, non su stdout)
log: (...args: unknown[]) => void
warn: (...args: unknown[]) => void
error: (...args: unknown[]) => void
}
}Passo 4: Applicazione del Timeout
Ogni esecuzione di action è avvolta in un Promise.race() con un timeout configurabile (default 5 secondi). Se l’action non si risolve entro il timeout, viene trattata come un fallimento.
Passo 5: Elaborazione del Risultato
interface ActionResult {
allow?: boolean // false = blocca il login/registrazione
claims?: Record<string, unknown> // Aggiunge claim personalizzati al JWT
metadata?: Record<string, unknown> // Aggiunge metadata al record utente
reason?: string // Motivo di rifiuto leggibile (mostrato all'utente)
}La Pipeline dei Trigger
Le Actions sono organizzate per trigger — il punto nel flusso di autenticazione dove vengono eseguite:
| Trigger | Quando si Attiva | Può Bloccare? | Può Arricchire Token? |
|---|---|---|---|
pre_login | Dopo la validazione delle credenziali, prima della creazione della sessione | Sì | No |
post_login | Dopo il login riuscito, prima dell’emissione del token | No | Sì (aggiungi claim) |
pre_signup | Dopo la validazione del form di registrazione, prima della creazione utente | Sì | No |
post_signup | Dopo la creazione dell’utente nel database | No | Sì (aggiungi metadata) |
post_change_password | Dopo il cambio password completato | No | No |
pre_m2m_token | Prima di emettere un token M2M via client_credentials | Sì | Sì (aggiungi claim) |
Esecuzione Sequenziale
Più actions possono essere registrate per lo stesso trigger. Vengono eseguite sequenzialmente nell’ordine definito dal campo order. Se un’action restituisce { allow: false }, le successive vengono saltate.
Se un’action lancia un’eccezione non gestita, il flusso viene bloccato per default (fail-secure). Questo previene che un’action rotta consenta accidentalmente accessi non autorizzati.
Se vuoi che un’action specifica sia non bloccante (solo advisory), avvolgi il codice dell’action in un try-catch e restituisci sempre { allow: true }. In questo modo, le eccezioni vengono catturate all’interno dell’action e il flusso continua.
Il Modello di Sicurezza in Dettaglio
Cosa le Actions POSSONO Fare
| Capacità | Come |
|---|---|
| Fare chiamate HTTP outbound | await context.fetch('https://api.example.com/check', { ... }) |
| Leggere il profilo dell’utente corrente | context.user.email, context.user.roles |
| Leggere i metadati della richiesta | context.request.ip, context.request.userAgent |
| Bloccare l’autenticazione | return { allow: false, reason: 'Bloccato dalla policy' } |
| Aggiungere claim JWT | return { claims: { department: 'engineering' } } |
| Loggare messaggi | context.console.log('Controllato IP:', context.request.ip) |
Cosa le Actions NON POSSONO Fare
| Restrizione | Perché |
|---|---|
Caricare moduli (require, import) | Previene accesso alle API Node.js (fs, child_process, net, ecc.) |
Accedere a process | Previene la lettura di variabili env, l’uscita dal server |
Accedere a global / globalThis | Previene l’uscita dallo scope della sandbox |
| Accedere al filesystem | Nessun modulo fs disponibile |
| Accedere al database | Nessun client Prisma o connessione db nello scope |
| Accedere ai dati di altri tenant | context contiene solo dati del tenant corrente |
| Eseguire indefinitamente | Timeout Promise.race() applicato (default 5 secondi) |
Caratteristiche di Performance
| Operazione | Latenza Tipica | Note |
|---|---|---|
| Analisi statica | <0.1ms | Semplice ricerca di stringhe |
| Costruzione della funzione | ~0.5ms | Una volta per esecuzione |
| Logica semplice (nessun HTTP) | 1-5ms | Condizionali, operazioni sulle stringhe |
| Con una chiamata HTTP outbound | 50-500ms | Dominata dalla latenza del servizio esterno |
| Timeout | 5.000ms (default) | Configurabile per action (1s-30s) |
L’Editor Blueprint Visuale
Per i team che preferiscono la programmazione visuale rispetto alla scrittura di JavaScript, Auris fornisce un Blueprint Editor — un editor visuale basato su nodi.
Tipi di Nodo
| Tipo di Nodo | Equivalente JavaScript |
|---|---|
| Trigger | Punto di ingresso della funzione |
| Condizione | if (campo operatore valore) |
| Logic Gate | && (AND) o || (OR) |
| Deny Action | return { allow: false, reason: '...' } |
| Set Claims | return { claims: { chiave: valore } } |
| Set Metadata | return { metadata: { chiave: valore } } |
| Log | context.console.log('...') |
L’Editor Blueprint mantiene una mappatura bidirezionale tra il grafo visuale e il codice JavaScript. Non tutto il JavaScript è rappresentabile nell’editor visuale — la logica complessa (loop, ricorsione, pattern async) richiede la modifica solo in codice.
Esempi di Codice: Pattern Action Comuni
Bloccare Provider Email Temporanei
// Trigger: pre_signup
const disposableDomains = [
'tempmail.com', 'throwaway.email', 'guerrillamail.com',
'mailinator.com', '10minutemail.com', 'yopmail.com'
]
const domain = context.user.email.split('@')[1]
if (disposableDomains.includes(domain)) {
return {
allow: false,
reason: 'Gli indirizzi email temporanei non sono consentiti.'
}
}
return { allow: true }Arricchire il Token con Dati Esterni
// Trigger: post_login
try {
const response = await context.fetch(
'https://hr.internal/api/employee?email=' +
encodeURIComponent(context.user.email),
{ headers: { 'X-Api-Key': 'hr-api-key' } }
)
if (response.ok) {
const employee = await response.json()
return {
claims: {
department: employee.department,
manager_id: employee.managerId,
cost_center: employee.costCenter
}
}
}
} catch (err) {
context.console.error('Chiamata API HR fallita:', err.message)
}
// Se l'API HR non risponde, non bloccare il login
return {}Restrizione Geografica del Login
// Trigger: pre_login
const allowedCountries = ['US', 'CA', 'GB', 'DE', 'FR', 'IT', 'ES']
const country = context.request.geoip?.country
if (country && !allowedCountries.includes(country)) {
context.console.warn('Login bloccato da', country, 'per utente', context.user.email)
return {
allow: false,
reason: 'Il login non è disponibile dalla tua posizione attuale.'
}
}
return { allow: true }Concetti Correlati
- OAuth 2.0 & OIDC — I flussi di autenticazione a cui le Actions si agganciano
- Token Spiegati — Come i claim personalizzati delle Actions appaiono nei JWT
- MFA Adattivo & Risk Scoring — Il risk scoring può innescare MFA step-up, complementando le Actions
- Multi-Tenancy — Come l’isolamento dei tenant si applica alle Actions