au1h
Concepts

Security model

What au1h protects, how, and what you still need to do.

Trust boundaries

  • The application slug is public. It selects the tenant, like an OAuth client_id. Knowing a slug lets you sign up or sign in as a user of that app, never as another app's user and never as an admin.
  • The application secret is private. It authenticates your backend to the server-to-server API. It is stored as a scrypt hash and shown once.
  • The JWT audience is the slug. Your backend accepts only tokens minted for it.
  • The admin portal is a tenant too, reserved as au1h-admin. Admin sign-up is open only for the very first admin, for emails in AU1H_ADMIN_EMAILS, or for pending workspace invitations. Nothing is inferred from Origin or Referer.
  • The admin API (/api/v1/*) requires an admin session and is scoped to the caller's workspace: applications, users and routes of other workspaces are invisible.

Isolation

Tenant scoping lives in the database adapter, below Better Auth, and in the Redis session cache. Every read, write, update and delete on users, sessions, accounts, organizations, members, invitations and verification tokens is confined to the request's application. Unit tests cover cross-tenant reads, writes by known id, the admin scope, transactions and concurrent interleaved requests; integration tests against Postgres cover the email flows and CORS.

Browser origins are scoped the same way. An application's allowed origins grant credentialed cross-site access to that application only, Better Auth's CSRF and callback-URL checks trust only the requesting application's origins, and the admin API trusts only CORS_ORIGIN. Registering an origin in your workspace cannot be used to drive another admin's browser against their workspace. Origins are validated as bare scheme://host[:port]; no paths or wildcards.

Emailed links (verification, reset, magic link) carry the application and their tokens are bound to it; see Email.

Credentials and secrets

  • Passwords: Better Auth's scrypt hashing.
  • Application secrets: scrypt-hashed at rest; the plaintext is never retrievable.
  • JWT signing key: Ed25519, generated on first use, private key encrypted with BETTER_AUTH_SECRET.
  • Logs redact tokens, passwords, secrets and cookie headers.

Rate limits

Redis-backed, shared across instances: /api/v1/*, /proxy/* and /api/apps/* by client IP (plus app slug where relevant). Better Auth's own limiter protects /api/auth/* in production. The client IP is the socket address, or with AU1H_TRUST_PROXY=true the last hop of x-forwarded-for (the one your proxy appended); clients cannot choose their own bucket, and the same IP is forwarded to backends behind the gateway.

Proxy targets

Route backends must be http(s) without embedded credentials. In production, loopback, private and link-local addresses (including IPv4-mapped IPv6 forms) are refused so au1h cannot be used to reach internal services, and backend redirects are relayed to the client rather than followed. The check is on the literal hostname; DNS rebinding is not detected.

The gateway's x-au1h-token carries typ: "au1h-proxy". The SDKs accept it only as gateway proof, compare it against x-user-id and x-user-email, and reject it as a Bearer token, so a user's own access token cannot forge gateway headers.

What you must still do

  • Run au1h behind TLS and set NODE_ENV=production.
  • Keep backends behind the gateway on a private network, or verify x-au1h-token.
  • Treat AU1H_ADMIN_EMAILS as the list of people who may administer every application.
  • Report vulnerabilities privately; see SECURITY.md in the repository.

On this page