Skip to Content

Architecture

This page describes how Auris is structured internally and how its components interact. Understanding the architecture helps when debugging integration issues, planning security boundaries, or evaluating Auris for enterprise deployment.

Auth planes (AURIS-MSP-SOV-3 / Auth constraint #1): Public Architecture does not list Keycloak as credential store, session manager, IdP federation broker, or token issuer for Auris tenants. Native Auris issuer mints and locally verifies JWTs with opaque tenant_id. IdP Admin sync/read seams are gone (fail-closed if AURIS_KEYCLOAK_IDP_ADMIN_SYNC / AURIS_KEYCLOAK_IDP_ADMIN_READ set ON). Admin user CRUD / password-reset native (seam-removal 2; AURIS_KEYCLOAK_ADMIN_USER_CRUD ON hard-fails). OP logout KC path gone (seam-removal 3; AURIS_KEYCLOAK_OP_LOGOUT ON hard-fails). This is not a Keycloak-zero / Auth #1 / constraint #9 claim — see docs/operations/keycloak-introspection-fail-closed.md.


High-Level Overview

Auris is a layered platform. Your application communicates with the Auris API and SDKs.

Auth planes (native Auris issuer + residual seams):

  • Token issuer (Auris) — Auris mints access/refresh JWTs (createTokens in apps/api/lib/auth/jwt.ts) with immutable opaque tenant_id. Issuer (iss) is the Auris API origin (OIDC discovery / AURIS_ISSUER), not a Keycloak URL. Verification is local (HS256/RS256) via verifyJWT / decodeAuthToken; Keycloak introspection is fail-closed.
  • Session / logout (Auris authoritative) — Auris RefreshToken families, rotation, and absolute TTL. Logout clears Auris session/cookies first; best-effort federated IdP end_session uses DB-sourced discovery/endSessionUrl (never Keycloak /broker/ logout). Keycloak OP refresh logout removed (seam-removal 3; AURIS_KEYCLOAK_OP_LOGOUT ON hard-fails).
  • IdP federation (native SP + DB SoT) — Social/OIDC start+callback resolve credentials from Auris DB (TenantIdentityProvider / SsoConnection); admin IdP/SSO CRUD writes Auris DB only. Keycloak Admin IdP sync/read removed (seam-removal 1; flags ON hard-fail). Full native SAML SP remains deferred (fail-closed stub).
  • Credential store posture — Tenant IdP client secrets live in Auris DB (encrypted at rest when IDENTITY_ENCRYPTION_KEY is set; fail-closed in staging/prod). Password login ROPC is fail-closed (passwordless). Admin user CRUD / password-reset is native (TenantUser + session revoke; seam-removal 2; no KC Admin credential-store writes). Not described as Keycloak credential store / session manager / federation broker / token issuer. Not Keycloak-zero.

Also: Prisma RBAC (action:resource), optional native FGA, SCIM, Vault, hosted login UI, attack protection, developer API.


Tenant Model

Every Auris deployment supports multiple tenants. A tenant is a fully isolated environment with its own:

  • User accounts and credentials
  • Applications (registered OAuth 2.0 clients)
  • Roles, permissions, and authorization policies
  • Security configuration (rate limits, MFA policy, IP rules, CAPTCHA)
  • Email templates, branding, and custom domains
  • Audit logs and webhook endpoints

Hard isolation is the Auris Tenant (Tenant.id, opaque). A routing slug (Tenant.keycloakRealm column name is historical) may appear in iss path /realms/<slug> and as tenant_slug — it is not an authz key. Users in tenant A cannot obtain tokens for tenant B. ERP product rows use Tenant.id; licensing may still key off the slug as licenseeId. An Organization lives under a tenant — it is not the isolation key for ERP rows.

The x-tenant header, where accepted, may carry slug or id. Middleware treats it as a consistency check against the verified JWT tenant_id (mismatch → 403). Do not trust a client-supplied x-tenant as authorization.

The string 'default' is an SDK-level fallback for unscoped local development only. Production deployments must always pass the real Tenant ID. Using 'default' as a hardcoded tenant can silently disable security features like attack protection and MFA enforcement.


Three-Layer Authorization

Auris implements authorization in three layers, each adding more granularity than the previous:

Layer 1 — Authentication (Auris-minted JWTs)

Access tokens are minted and verified by Auris. Signing is JWT_SECRET HS256 by default, or RS256 when JWT_ALGORITHM=RS256 and a JwkKeyPair row exists. Opaque tenant_id is always present on tenant-scoped access tokens. Raw foreign IdP tokens are not accepted for API authz (introspection fail-closed).

Claims on Auris-minted access tokens include iss (Auris issuer + /realms/<slug> for routing), sub, email, tenant_id (opaque), realm / tenant_slug (routing only), roles, optional org_id / org, optional tenants. See apps/api/lib/auth/jwt.ts and the claim-shape ADR. Do not assume realm_access.roles is present.

Layer 2 — Prisma RBAC (Fine-Grained Roles with Tri-State Permissions)

Auris maintains its own role and permission store in PostgreSQL (via Prisma). This layer provides:

  • Roles with a named set of permissions
  • Permissions in action:resource format (e.g., manage:users, view:invoices, approve:expenses)
  • Tri-state permission values: ALLOW, DENY, or INHERIT
    • ALLOW — explicitly grants the permission
    • DENY — explicitly revokes it, even if another role grants it (DENY wins)
    • INHERIT — falls back to the parent role or tenant default
  • Per-user permission overrides that can grant or revoke specific permissions regardless of role
  • Application-scoped permissions — the same user can have different permissions in different registered applications

Permission checks happen server-side via POST /api/roles/check. The dashboard enforces permissions on every API route using requirePermission(req, 'action:resource').

Layer 3 — FGA (Fine-Grained Authorization / Zanzibar-style ReBAC)

The FGA layer implements object-level, relationship-based access control, inspired by Google’s Zanzibar paper. It answers questions like “can user Alice read document 42?” or “is user Bob a member of organization X?”.

The FGA model consists of:

  • Authorization Model — a schema (written in OpenFGA DSL) defining object types and the relations between them
  • Relationship Tuples — facts stored in the database (e.g., document:42#viewer@user:alice)
  • Check engine — a recursive Zanzibar algorithm that evaluates whether a subject has a relation to an object, following computed userset and tuple-to-userset rewrites up to a configurable depth (default: 25)

FGA supports six rewrite rule types: this, computedUserset, tupleToUserset, union, intersection, and exclusion. It also supports expand (list all subjects for a relation) and listObjects (list all objects a subject has access to via reverse lookup).

When FGA_ENGINE_ENABLED=true, resource access checks are routed to the FGA engine. The legacy Ory Keto integration remains as a fallback and is deprecated.


Application Types

Auris supports four application types, corresponding to standard OAuth 2.0 client profiles:

TypeDescriptionAuth Flow
WEBBrowser-based apps (SPAs, server-rendered)Authorization Code + PKCE
MOBILENative iOS / Android appsAuthorization Code + PKCE
APIResource servers that validate tokensToken introspection / JWKS
M2MServer-to-server, background jobs, CLI toolsClient Credentials grant

Each application is assigned a Client ID (public) and optionally a Client Secret (for confidential clients). Redirect URIs are registered per application and enforced exactly — no wildcard matching.

For M2M applications, scopes control what the token allows (e.g., read:users, write:invoices). Custom JWT claims can be attached per application to embed additional data into access tokens without an extra API call.


Token Flow

Auris issues three types of tokens following the OpenID Connect specification:

Access Token

  • Format: JWT, code default JWT_ALGORITHM=HS256. RS256 is used when that env is set and a JwkKeyPair exists. Deployed algorithm is UNKNOWN from git alone.
  • Lifetime: JWT_EXPIRATION default 3600 s (apps/api/lib/auth/jwt.ts).
  • Contains (Auris-minted): iss, sub, email, tenant_id (opaque), realm / tenant_slug, roles, iat, exp, optional org_id, tenants, acr/amr, custom application claims.
  • Usage: Authorization: Bearer <token>
  • Verification: local HS256/RS256 only (verifyJWT). No Keycloak introspection. JWKS at /api/.well-known/jwks.json applies to the RS256 path.

Refresh Token

  • Format: JWT with type: 'refresh' (not an opaque random string). Persisted in RefreshToken by jti + family.
  • Lifetime: 30 days in createTokens.
  • Usage: POST /api/auth/token grant refresh_token (Auris rotation). Prefer this path over any legacy provider refresh.
  • Security: single-use rotation with reuse detection (refresh-token-service.ts).

ID Token

  • Format: JWT, same signing config as access tokens (default HS256, not “always RS256”).
  • Contains: identity claims for the client UI — not sent to APIs.

OIDC Discovery

Auris publishes a standard OIDC Discovery document at:

GET /api/.well-known/openid-configuration

This endpoint returns the authorization endpoint, token endpoint, JWKS URI, supported grant types, scopes, and signing algorithms. Standard OIDC clients can auto-configure from this document.

The JWKS endpoint:

GET /api/.well-known/jwks.json

Returns the current public key set in JWK format when RS256 keys exist. Code default signing is HS256 (JWT_SECRET), which does not use JWKS. Do not tell integrators “always verify RS256 via JWKS” unless JWT_ALGORITHM=RS256 is actually deployed.


Hosted Login Pages

Auris provides hosted login pages served from the Auris domain (or your custom domain). These pages handle the OAuth 2.0 Authorization Code + PKCE flow end-to-end:

  1. Your application calls loginWithRedirect() (SDK) or redirects to GET /api/oauth/authorize
  2. The user sees the Auris-hosted login page, styled with your tenant’s branding (logo, colors, favicon)
  3. After successful authentication, Auris redirects back to your redirect_uri with an authorization code
  4. Your application exchanges the code for tokens at POST /api/auth/token (handled automatically by SDKs)
  5. The SDK stores the access token and refresh token, and the user is authenticated

All security layers (CAPTCHA, rate limiting, brute-force protection, suspicious login detection, adaptive MFA) are applied during the hosted login flow. You do not need to implement these on your own login pages.


Actions Engine

Actions are custom JavaScript functions that run during authentication flows. They execute in a sandboxed environment with a restricted scope — require, import, process, eval, fs, and child_process are blocked.

Actions can be triggered at six points:

TriggerWhen it fires
pre_loginBefore primary authentication completes
post_loginAfter successful authentication, before token issuance
pre_signupBefore a new user account is created
post_signupAfter a new user account is created
post_change_passwordAfter a password change
pre_m2m_tokenBefore issuing a machine-to-machine token

Actions have access to a context object containing the user, tenant, application, and event data. They can deny the auth flow, enrich the token with additional claims, or call external services.


Security Architecture

Security is applied in layers, from network to application:

Rate Limiting

Four rate limit tiers with sliding-window counters:

TierApplied toDefault limit
authLogin, signup, password reset10 requests / 15 minutes
sensitive2FA, magic links, phone verify5 requests / hour
apiAuthenticated API calls100 requests / minute
publicUnauthenticated public endpoints300 requests / minute

Rate limit state uses in-memory counters. Standard Retry-After and X-RateLimit-* headers are returned on 429 responses.

Brute Force Protection

Failed login attempts are tracked per account. After a configurable threshold (default: 5 failures in 15 minutes), the account is temporarily locked. Administrators can view and unlock accounts from the console.

CAPTCHA

Three providers are supported: Cloudflare Turnstile, hCaptcha, and Google reCAPTCHA v3. CAPTCHA can be configured to trigger always, only on suspicious requests, or after a number of failed login attempts. CAPTCHA verification is enforced server-side — client-side tokens are validated against the provider’s API before authentication proceeds.

IP Rules

Each tenant can define CIDR-based allow and block rules. Rules can be scoped to the entire tenant or to a specific application. Block rules are evaluated first; if a request IP matches a block rule, it is rejected before any authentication logic runs.

Suspicious Login Detection

After each successful authentication, Auris evaluates five risk signals:

  • New device — the device fingerprint has not been seen before for this user
  • New IP — the IP address has not been used before for this user
  • New country — the geographic country differs from previous sessions
  • Impossible travel — the distance between this login and the previous one is too large for the elapsed time
  • VPN / proxy / datacenter IP — the IP is identified as a VPN exit node, open proxy, or datacenter address

If any signal triggers, a SuspiciousLoginEvent is recorded and an alert notification is sent. Depending on tenant configuration, suspicious logins can be blocked, challenged with step-up MFA, or allowed with logging only.

DPoP Token Binding

For applications requiring the highest level of token security, Auris supports Demonstrating Proof of Possession (DPoP, RFC 9449). DPoP binds access tokens to a client’s public key, preventing stolen tokens from being used by a different client. The dpop-validator middleware validates DPoP proofs on every request for applications with DPoP enabled.


Data Model Summary

The core entities in Auris and their relationships:

All Prisma model changes (new fields or tables) require npx prisma generate and npx prisma db push to take effect. Pre-existing model errors in the networking section resolve automatically after running these commands.