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 (
createTokensinapps/api/lib/auth/jwt.ts) with immutable opaquetenant_id. Issuer (iss) is the Auris API origin (OIDC discovery /AURIS_ISSUER), not a Keycloak URL. Verification is local (HS256/RS256) viaverifyJWT/decodeAuthToken; Keycloak introspection is fail-closed. - Session / logout (Auris authoritative) — Auris
RefreshTokenfamilies, rotation, and absolute TTL. Logout clears Auris session/cookies first; best-effort federated IdPend_sessionuses DB-sourced discovery/endSessionUrl(never Keycloak/broker/logout). Keycloak OP refresh logout removed (seam-removal 3;AURIS_KEYCLOAK_OP_LOGOUTON 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_KEYis 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:resourceformat (e.g.,manage:users,view:invoices,approve:expenses) - Tri-state permission values:
ALLOW,DENY, orINHERITALLOW— explicitly grants the permissionDENY— 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:
| Type | Description | Auth Flow |
|---|---|---|
WEB | Browser-based apps (SPAs, server-rendered) | Authorization Code + PKCE |
MOBILE | Native iOS / Android apps | Authorization Code + PKCE |
API | Resource servers that validate tokens | Token introspection / JWKS |
M2M | Server-to-server, background jobs, CLI tools | Client 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 aJwkKeyPairexists. Deployed algorithm is UNKNOWN from git alone. - Lifetime:
JWT_EXPIRATIONdefault 3600 s (apps/api/lib/auth/jwt.ts). - Contains (Auris-minted):
iss,sub,email,tenant_id(opaque),realm/tenant_slug,roles,iat,exp, optionalorg_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.jsonapplies to the RS256 path.
Refresh Token
- Format: JWT with
type: 'refresh'(not an opaque random string). Persisted inRefreshTokenbyjti+family. - Lifetime: 30 days in
createTokens. - Usage:
POST /api/auth/tokengrantrefresh_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-configurationThis 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.jsonReturns 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:
- Your application calls
loginWithRedirect()(SDK) or redirects toGET /api/oauth/authorize - The user sees the Auris-hosted login page, styled with your tenant’s branding (logo, colors, favicon)
- After successful authentication, Auris redirects back to your
redirect_uriwith an authorization code - Your application exchanges the code for tokens at
POST /api/auth/token(handled automatically by SDKs) - 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:
| Trigger | When it fires |
|---|---|
pre_login | Before primary authentication completes |
post_login | After successful authentication, before token issuance |
pre_signup | Before a new user account is created |
post_signup | After a new user account is created |
post_change_password | After a password change |
pre_m2m_token | Before 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:
| Tier | Applied to | Default limit |
|---|---|---|
auth | Login, signup, password reset | 10 requests / 15 minutes |
sensitive | 2FA, magic links, phone verify | 5 requests / hour |
api | Authenticated API calls | 100 requests / minute |
public | Unauthenticated public endpoints | 300 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.