Skip to Content
GuidesWriting Custom Actions

Writing Custom Actions

Actions are custom JavaScript functions that execute at specific points in the Auris authentication pipeline. They allow you to extend the platform’s behavior without forking or modifying Auris itself — add custom claims to tokens, block logins based on external data, call webhooks, enforce business rules, or integrate with third-party services.

When to use Actions vs other extension points

MechanismBest For
ActionsLogic that must run synchronously during the auth flow (block a login, add claims, modify user metadata)
Custom ClaimsStatic or attribute-based token enrichment that does not require external calls or conditional logic
WebhooksAsynchronous notifications after events occur (audit logging, analytics, Slack alerts)

If your use case requires an external API call that must succeed before the login completes, use an Action. If you only need to notify a system after the fact, use a Webhook.


The Sandbox Environment

Actions execute in a restricted JavaScript sandbox. The sandbox provides:

Available APIs

APINotes
fetch()Make HTTP requests to external services. Full Fetch API.
JSONJSON.parse() and JSON.stringify()
DateDate construction and manipulation
MathMathematical operations
console.log()Output is captured and visible in action logs
String, Number, Boolean, Array, ObjectStandard built-in types
Promise, async/awaitAsynchronous code is fully supported
URL, URLSearchParamsURL parsing and construction
TextEncoder, TextDecoderText encoding utilities
crypto.randomUUID()UUID generation

Blocked APIs

The following are blocked for security:

BlockedReason
require(), importNo module system access
processNo access to environment variables or process info
Dynamic code execution functionsNo runtime code generation
global, globalThisNo access to the global scope
fs, child_processNo filesystem or process spawning

Actions have a configurable execution timeout (default: 5 seconds, max: 30 seconds). If an action exceeds the timeout, it is terminated and the configured failure behavior applies (allow or deny the request).


Trigger Types

Actions fire at specific points in the authentication flow. Each trigger receives a different context object:

TriggerWhen It FiresCommon Uses
pre-loginBefore credentials are verifiedBlock logins by email domain, check external blocklists
post-loginAfter successful authentication, before token issuanceAdd custom claims, sync with external systems, conditional MFA
pre-registerBefore a new user is createdValidate email domain, check invitation requirements
post-registerAfter a new user is createdSend welcome webhook, assign to organization, set metadata
post-change-passwordAfter a password changeNotify external systems, invalidate cached sessions
pre-tokenBefore an M2M token is issuedRestrict scopes, validate client against external rules

The Context Object

Every action receives a context object with information about the current request. The shape varies by trigger:

pre-login and 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-token

{ client: { id: 'client-abc', name: 'Billing Service', type: 'M2M', }, requestedScopes: ['read:users', 'manage:billing'], tenant: { id: 'tenant-789', name: 'acme-corp' }, }

The ActionResult Return Type

Actions return an object that controls the outcome:

interface ActionResult { allow: boolean // true = proceed, false = block the request message?: string // Error message shown to the user when allow=false claims?: Record<string, any> // Custom claims to add to the token (post-login, pre-token) metadata?: Record<string, any> // Metadata to set on the user record }

If an action does not return a result (or returns undefined), the request proceeds as normal.


Practical Examples

Block logins by email domain

// Trigger: pre-login // Block personal email providers from logging in 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: 'Personal email addresses are not allowed. Please use your corporate email.', } } return { allow: true }

Add custom claims based on roles

// Trigger: post-login // Add a 'plan' claim and feature flags based on user metadata 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, }, }

Log authentication events to an external webhook

// Trigger: post-login // Send a webhook notification for every login 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) { // Non-critical -- log but don't block the login console.log('Webhook failed:', err.message) } return { allow: true }

Restrict M2M scopes based on external policy

// Trigger: pre-token // Check an external policy service before issuing M2M tokens 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: 'Token request denied by policy service.', } } const policy = await response.json() return { allow: true, claims: { scope: policy.allowedScopes.join(' '), 'policy-version': policy.version, }, }

Conditional MFA trigger

// Trigger: post-login // Require MFA for admin roles or logins from new countries 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 }, }

Execution Order

When multiple actions are configured for the same trigger, they execute in the order defined by their order field (ascending). Each action receives the same original context — one action’s output does not modify the context for subsequent actions.

However, claims and metadata from all actions are merged. If two actions set the same claim key, the last action in order wins.

Action OrderAction NameClaims Output
1Add plan claim{ plan: 'pro' }
2Add feature flags{ features: { ... } }
3Add region claim{ region: 'eu' }

Final token claims: { plan: 'pro', features: { ... }, region: 'eu' }

If any action returns allow: false, the entire pipeline is halted and the request is denied, regardless of subsequent actions.


The Blueprint Visual Editor

For teams that prefer a visual approach, Auris provides a Blueprint editor — a node-based visual rule builder inspired by Unreal Engine’s Blueprint system.

The Blueprint editor represents actions as a graph of connected nodes:

Node TypeColorPurpose
TriggerOrangeThe entry point — which auth event fires the rule
ConditionCyanCheck a field against a value (e.g., user.email contains @acme.com)
Logic GateVioletCombine conditions with AND/OR
DenyPinkBlock the request with an error message
Set ClaimsBlueAdd key-value pairs to the token
Set MetadataEmeraldUpdate user metadata
LogGreenWrite to action logs

The Blueprint editor and the code editor are two views of the same underlying action. Changes in one are synchronized to the other. You can start with Blueprint for simple rules and switch to code for complex logic.

To access the Blueprint editor, open an action’s detail page in the Console and click the Blueprint tab.


Debugging Actions

Action Logs

Every action execution is logged. View logs in Console -> Actions -> [Action Name] -> Logs tab. Each log entry includes:

  • Execution timestamp
  • Trigger event
  • Execution duration (ms)
  • Status (success, error, timeout)
  • Console.log output
  • Return value

Using console.log

console.log() output is captured and stored in the action log. Use it for debugging:

console.log('User roles:', JSON.stringify(context.user.roles)) console.log('Request IP:', context.request.ip) console.log('GeoIP data:', JSON.stringify(context.request.geoip)) // This output appears in the action's Logs tab return { allow: true }

Error Handling

If an action throws an unhandled error, the behavior depends on the action’s failure mode setting:

Failure ModeBehavior
allow (default)The request proceeds. The error is logged.
denyThe request is blocked with a generic error message.

Always use try/catch around external API calls to prevent failures from blocking logins:

try { const result = await fetch('https://external-api.example.com/check') // process result } catch (err) { console.log('External API failed, proceeding:', err.message) // Don't block the login because an external service is down } return { allow: true }

Performance Tips

  1. Keep actions fast — Aim for less than 100ms execution time. Every millisecond adds to login latency.
  2. Use try/catch for external calls — Never let a non-critical external API failure block a login.
  3. Avoid sequential external calls — If you need multiple API calls, use Promise.all() to run them in parallel.
  4. Cache expensive lookups — For data that changes infrequently, consider caching in user metadata and refreshing periodically rather than querying on every login.
  5. Set appropriate timeouts — Use AbortController with fetch() to set explicit timeouts shorter than the action timeout:
const controller = new AbortController() setTimeout(() => controller.abort(), 3000) // 3 second timeout const response = await fetch('https://slow-api.example.com/check', { signal: controller.signal, })

API Endpoints

GET/api/actionsRequires: admin:all

List all actions for the tenant. Supports filtering by trigger type and status.

POST/api/actionsRequires: admin:all

Create a new action. Requires name, trigger, code, and optional order, timeout, status.

GET/api/actions/:idRequires: admin:all

Get action details including code, trigger, execution count, and error count.

PATCH/api/actions/:idRequires: admin:all

Update an action’s code, trigger, order, timeout, or status.

DELETE/api/actions/:idRequires: admin:all

Delete an action.

GET/api/actions/:id/logsRequires: admin:all

List execution logs for a specific action. Includes duration, status, console output, and return value.


Required Permissions

OperationPermission
View actionsadmin:all
Create, edit, delete actionsadmin:all
View action logsadmin:all