Skip to Content

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:

TriggerQuando si AttivaPuò Bloccare?Può Arricchire Token?
pre_loginDopo la validazione delle credenziali, prima della creazione della sessioneSìNo
post_loginDopo il login riuscito, prima dell’emissione del tokenNoSì (aggiungi claim)
pre_signupDopo la validazione del form di registrazione, prima della creazione utenteSìNo
post_signupDopo la creazione dell’utente nel databaseNoSì (aggiungi metadata)
post_change_passwordDopo il cambio password completatoNoNo
pre_m2m_tokenPrima di emettere un token M2M via client_credentialsSì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 outboundawait context.fetch('https://api.example.com/check', { ... })
Leggere il profilo dell’utente correntecontext.user.email, context.user.roles
Leggere i metadati della richiestacontext.request.ip, context.request.userAgent
Bloccare l’autenticazionereturn { allow: false, reason: 'Bloccato dalla policy' }
Aggiungere claim JWTreturn { claims: { department: 'engineering' } }
Loggare messaggicontext.console.log('Controllato IP:', context.request.ip)

Cosa le Actions NON POSSONO Fare

RestrizionePerché
Caricare moduli (require, import)Previene accesso alle API Node.js (fs, child_process, net, ecc.)
Accedere a processPreviene la lettura di variabili env, l’uscita dal server
Accedere a global / globalThisPreviene l’uscita dallo scope della sandbox
Accedere al filesystemNessun modulo fs disponibile
Accedere al databaseNessun client Prisma o connessione db nello scope
Accedere ai dati di altri tenantcontext contiene solo dati del tenant corrente
Eseguire indefinitamenteTimeout Promise.race() applicato (default 5 secondi)

Caratteristiche di Performance

OperazioneLatenza TipicaNote
Analisi statica<0.1msSemplice ricerca di stringhe
Costruzione della funzione~0.5msUna volta per esecuzione
Logica semplice (nessun HTTP)1-5msCondizionali, operazioni sulle stringhe
Con una chiamata HTTP outbound50-500msDominata dalla latenza del servizio esterno
Timeout5.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 NodoEquivalente JavaScript
TriggerPunto di ingresso della funzione
Condizioneif (campo operatore valore)
Logic Gate&& (AND) o || (OR)
Deny Actionreturn { allow: false, reason: '...' }
Set Claimsreturn { claims: { chiave: valore } }
Set Metadatareturn { metadata: { chiave: valore } }
Logcontext.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