Skip to Content

Actions & Sandbox-Ausführung

Das Problem: Erweiterbarkeit ohne Forking

Jede Identitätsplattform begegnet letztendlich der gleichen Spannung: Kunden benötigen benutzerdefinierte Logik in ihren Authentifizierungs-Flows, aber die Plattform kann nicht jede Anforderung antizipieren. Ohne einen Erweiterungsmechanismus sind die Optionen:

AnsatzProblem
Feature-RequestsLangsam, blockiert Kunden, jeder Anwendungsfall ist anders
Webhooks zu externen DienstenFügt Latenz hinzu, erfordert, dass der Kunde einen Webhook-Server hostet, kann den Flow nicht synchron blockieren
Plattform forkenWartungsalptraum, blockiert Upgrades
Nur-Konfigurations-RegelnZu begrenzt für reale Logik

Was Kunden tatsächlich brauchen, ist die Möglichkeit, benutzerdefinierten Code an bestimmten Punkten im Authentifizierungs-Flow auszuführen — vor der Anmeldung (um verdächtige Versuche zu blockieren), nach der Anmeldung (um Tokens mit benutzerdefinierten Claims anzureichern), nach der Registrierung (um Onboarding-Workflows auszulösen) und mehr.

Die Herausforderung besteht darin, dies sicher zu tun. Kundencode läuft innerhalb des Autorisierungsservers, dem sicherheitskritischsten Komponenten der gesamten Infrastruktur. Ein Fehler in benutzerdefiniertem Code darf den Server nicht zum Absturz bringen, Daten von anderen Tenants nicht preisgeben oder Authentifizierungskontrollen umgehen.

Branchenansätze

Verschiedene Identitätsanbieter lösen dieses Problem auf grundlegend unterschiedliche Weisen:

AnbieterMechanismusSpracheAusführungsmodellLatenzauswirkung
Auth0ActionsJavaScript/TypeScriptIsolierte VM (Deno-ähnliche Sandbox)Mittel (VM-Start)
OktaHooks (Inline/Event)K. A. (Webhook)Externer HTTP-Aufruf an KundendienstHoch (Netzwerk-Roundtrip)
FirebaseExtensionsTypeScriptCloud Functions (separater Prozess)Hoch (Cold Start + Ausführung)
KeycloakSPIs (Service Provider Interfaces)JavaClasspath-Plugin, beim Start geladenNiedrig (gleicher Prozess)
AurisActionsJavaScriptSandgeboxte Function-ConstructorNiedrig (gleicher Prozess, keine VM)

Jeder Ansatz macht einen anderen Kompromiss zwischen Isolation, Leistung und Entwicklererfahrung:

  • V8-Isolates (Auth0): Starke Isolation, aber VM-Start fügt Latenz und Infrastrukturkomplexität hinzu.
  • Webhooks (Okta): Vollständige Isolation (läuft auf der Infrastruktur des Kunden), aber fügt Netzwerklatenz hinzu und erfordert, dass der Kunde einen Server betreibt.
  • Classpath-Plugins (Keycloak): Null Overhead, erfordert aber Java-Expertise, komplexes Deployment und Neustart für Updates.
  • Function constructor (Auris): Sehr niedriger Overhead, läuft im gleichen Node.js-Prozess, mit Isolation auf Code-Analyse-Ebene statt Runtime-Ebene.

Funktionsweise der Auris-Sandbox

Auris Actions führen benutzerdefiniertes JavaScript in einer kontrollierten Umgebung mithilfe des JavaScript-Function-Constructors aus. Dieser Mechanismus erstellt eine Funktion aus einem Code-String mit einem expliziten Scope — der Action-Code sieht nur Variablen, die Auris explizit bereitstellt.

Die Ausführungspipeline

Schritt 1: Statische Analyse

Vor der Ausführung von Code scannt Auris die Quelle der Action auf blockierte Muster:

const BLOCKED_PATTERNS = [ 'require', // Kein Modul-Laden 'import', // Keine ES-Modul-Imports 'process', // Kein Zugriff auf process.env, process.exit usw. 'global', // Kein Zugriff auf das globale Objekt 'child_process', // Kein Subprocess-Spawning 'fs', // Kein Dateisystemzugriff ]

Wenn ein blockiertes Muster im Quellcode gefunden wird, wird die Action vor der Ausführung abgelehnt. Dies ist eine Defense-in-Depth-Maßnahme.

Statische Analyse ist allein keine Sicherheitsgrenze. Ein entschlossener Angreifer könnte blockierte Schlüsselwörter verschleiern. Die statische Analyse existiert, um versehentlichen Missbrauch zu erkennen und klare Fehlermeldungen bereitzustellen. Die echte Sicherheit kommt vom eingeschränkten Scope-Objekt, das der konstruierten Funktion bereitgestellt wird.

Schritt 2: Funktionskonstruktion

Der Action-Code-String wird verwendet, um eine Funktion mit context als einzigem Parameter zu erstellen:

// Konzeptionell äquivalent zu: function anonymous(context) { // actionCode läuft hier // Es kann NUR zugreifen auf: // - 'context' (das Argument) // - JavaScript-Built-ins (Array, Object, String, Math, Date, JSON usw.) // - fetch (explizit für ausgehende HTTP bereitgestellt) // - console (erfasst, nicht echtes stdout) }

Die kritische Eigenschaft: Die Funktion hat keinen Closure über die Variablen des Servers. Sie kann nicht auf require, process, __dirname, Datenbankverbindungen oder andere serverseitige Zustände zugreifen.

Schritt 3: Context-Objekt

Der context-Parameter stellt der Action Informationen über das aktuelle Authentifizierungsereignis und begrenzte Fähigkeiten bereit:

interface ActionContext { // Identitätsinformationen user: { id: string email: string username: string roles: string[] metadata: Record<string, unknown> } // Anfrageinformationen request: { ip: string userAgent: string geoip?: { country: string; city: string } } // Tenant-Informationen tenant: { id: string name: string } // Anwendungsinformationen application: { id: string clientId: string name: string } // Trigger-spezifische Daten trigger: string // 'pre_login' | 'post_login' | 'pre_signup' | 'post_signup' | ... // Erlaubte Utilities fetch: typeof fetch // Ausgehende HTTP (für Webhook-artige Integrationen) console: { // Erfasste Protokollierung (geht zu ActionLog, nicht stdout) log: (...args: unknown[]) => void warn: (...args: unknown[]) => void error: (...args: unknown[]) => void } }

Schritt 4: Timeout-Durchsetzung

Jede Action-Ausführung ist in ein Promise.race() mit einem konfigurierbaren Timeout (Standard 5 Sekunden) eingewickelt:

const result = await Promise.race([ fn(context), new Promise((_, reject) => setTimeout(() => reject(new Error('Action timed out')), action.timeout) ), ])

Schritt 5: Ergebnis-Verarbeitung

Der Rückgabewert der Action bestimmt, was als nächstes passiert:

interface ActionResult { // Pre-Trigger können den Flow blockieren allow?: boolean // false = Anmeldung/Registrierung blockieren // Post-Trigger können dem JWT benutzerdefinierte Claims hinzufügen claims?: Record<string, unknown> // Actions können Metadaten zum Benutzerdatensatz hinzufügen metadata?: Record<string, unknown> // Menschenlesbarer Ablehnungsgrund (dem Benutzer angezeigt, wenn allow=false) reason?: string }

Die Trigger-Pipeline

Actions sind nach Trigger organisiert — dem Punkt im Authentifizierungs-Flow, an dem sie ausgeführt werden:

TriggerWann er feuertKann blockieren?Kann Token anreichern?
pre_loginNach Anmeldedaten-Validierung, vor SitzungserstellungJaNein
post_loginNach erfolgreicher Anmeldung, vor Token-AusstellungNeinJa (Claims hinzufügen)
pre_signupNach Registrierungsformular-Validierung, vor BenutzererstellungJaNein
post_signupNach Benutzererstellung in der DatenbankNeinJa (Metadaten hinzufügen)
post_change_passwordNach PasswortänderungNeinNein
pre_m2m_tokenVor Ausstellung eines M2M-Tokens via client_credentialsJaJa (Claims hinzufügen)

Sequentielle Ausführung

Mehrere Actions können für denselben Trigger registriert werden. Sie werden sequentiell in der durch das order-Feld definierten Reihenfolge ausgeführt. Wenn Action 1 { allow: false } zurückgibt, werden Actions 2 und 3 übersprungen.

Wenn Action 2 eine unbehandelte Ausnahme wirft, wird der Flow standardmäßig blockiert (Fail-Secure). Dies verhindert, dass eine defekte Action versehentlich unbefugten Zugriff ermöglicht.

Wenn du möchtest, dass eine bestimmte Action nicht blockierend (nur beratend) ist, umhülle deinen Action-Code mit einem try-catch und gib immer { allow: true } zurück. So werden Ausnahmen innerhalb der Action abgefangen und der Flow wird fortgesetzt.

Das Sicherheitsmodell im Detail

Was Actions TUN KÖNNEN

FähigkeitWie
Ausgehende HTTP-Aufrufe tätigenawait context.fetch('https://api.example.com/check', { ... })
Aktuelles Benutzerprofil lesencontext.user.email, context.user.roles
Anfragemetadaten lesencontext.request.ip, context.request.userAgent
Authentifizierung blockierenreturn { allow: false, reason: 'Blocked by policy' }
JWT-Claims hinzufügenreturn { claims: { department: 'engineering' } }
Benutzermetadaten hinzufügenreturn { metadata: { onboardingStep: 3 } }
Nachrichten protokollierencontext.console.log('Checked IP:', context.request.ip)
JavaScript-Built-ins verwendenJSON.parse(), Date.now(), Math.random(), Array.from() usw.

Was Actions NICHT KÖNNEN

EinschränkungWarum
Module laden (require, import)Verhindert Zugriff auf Node.js-APIs (fs, child_process, net usw.)
Auf process zugreifenVerhindert Lesen von Env-Vars, Beenden des Servers
Auf global/globalThis zugreifenVerhindert das Entkommen aus dem Sandbox-Scope
Auf die Datenbank zugreifenKein Prisma-Client oder Datenbankverbindung im Scope
Auf Daten anderer Tenants zugreifencontext enthält nur Daten für den aktuellen Tenant und Benutzer
Unbegrenzt laufenPromise.race()-Timeout erzwungen (Standard 5 Sekunden)

Leistungsmerkmale

OperationTypische LatenzHinweise
Statische Analyse<0,1msEinfache String-Suche
Funktionskonstruktion~0,5msEinmalig pro Ausführung
Einfache Logik (kein HTTP)1-5msBedingte Anweisungen, String-Operationen
Mit einem ausgehenden HTTP-Aufruf50-500msDominiert durch externe Dienstlatenz
Timeout5.000ms (Standard)Pro Action konfigurierbar (1s-30s-Bereich)

Für ein typisches Setup mit einer Pre-Login-Action (IP-Prüfung, kein HTTP) und einer Post-Login-Action (benutzerdefinierte Claims hinzufügen, kein HTTP):

Anmeldung ohne Actions: ~150ms Anmeldung mit 2 Actions: ~155ms (+5ms) Anmeldung mit HTTP-Action: ~350ms (+200ms, dominiert durch HTTP-Aufruf)

Der Blueprint Visual Editor

Für Teams, die visuelle Programmierung gegenüber JavaScript-Schreiben bevorzugen, bietet Auris einen Blueprint-Editor — einen knotenbasierten visuellen Editor, inspiriert von Unreal Engines Blueprint-System.

Knotentypen

KnotentypVisuelles ErscheinungsbildJavaScript-Äquivalent
TriggerOrangener KnotenFunktionseinstiegspunkt
BedingungCyanfarbener Knotenif (field operator value)
LogikgatterVioletter Knoten&& (UND) oder `
Deny-ActionPinkfarbener Knotenreturn { allow: false, reason: '...' }
Claims setzenBlauer Knotenreturn { claims: { key: value } }
Metadaten setzenSmaragdgrüner Knotenreturn { metadata: { key: value } }
LogGrüner Knotencontext.console.log('...')

Der Blueprint-Editor pflegt eine bidirektionale Zuordnung zwischen dem visuellen Graphen und JavaScript-Code. Du kannst Actions visuell erstellen und dann zum Code-Editor wechseln, um das generierte JavaScript zu sehen (und zu ändern), und zurückwechseln, um deine Code-Änderungen visuell widergespiegelt zu sehen.

Codebeispiele: Häufige Action-Muster

Wegwerf-E-Mail-Anbieter blockieren

// Trigger: pre_signup // Registrierungen von Wegwerf-E-Mail-Anbietern blockieren 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: 'Wegwerf-E-Mail-Adressen sind nicht erlaubt.' } } return { allow: true }

Token mit externen Daten anreichern

// Trigger: post_login // Abteilung und Manager aus HR-System hinzufügen 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('HR-API-Aufruf fehlgeschlagen:', err.message) } // Wenn die HR-API nicht verfügbar ist, Anmeldung nicht blockieren — Anreicherung überspringen return {}

Geo-Einschränkung für Anmeldung

// Trigger: pre_login // Anmeldung nur aus bestimmten Ländern erlauben const allowedCountries = ['DE', 'AT', 'CH', 'US', 'GB', 'FR', 'IT'] const country = context.request.geoip?.country if (country && !allowedCountries.includes(country)) { context.console.warn( 'Anmeldung blockiert aus', country, 'für Benutzer', context.user.email ) return { allow: false, reason: 'Anmeldung ist von deinem aktuellen Standort nicht verfügbar.' } } return { allow: true }

Verwandte Konzepte