Server Configuration (v2)
Authorizer v2 uses CLI flags only for configuration.
Nothing is loaded from .env files or dashboard-managed env, and config is not persisted in the database or cache.
If you are migrating from v1, first skim the high-level Migration v1 to v2 guide and then use this page as a reference.
1. Core flags
./authorizer \
--env=production \
--http-port=8080 \
--host=0.0.0.0 \
--metrics-port=8081 \
--metrics-host=127.0.0.1 \
--log-level=info
--env: environment name (for exampleproduction,development).--http-port: HTTP listen port (default8080).--host: bind address for the main HTTP server (default0.0.0.0).--metrics-port: port for the dedicated/metricslistener (default8081; must differ from--http-port). Health probes stay on the HTTP port.--metrics-host: bind address for that dedicated metrics listener only (default127.0.0.1). The main app can listen on all interfaces while metrics stay on loopback. For Docker/Kubernetes scraping from another container/pod, set--metrics-host=0.0.0.0and keep the metrics port on an internal network only (never on a public load balancer).--log-level: one ofdebug,info,warn,error,fatal,panic.
2. Database and session store
Database
./authorizer \
--database-type=postgres \
--database-url="postgres://user:pass@host/db" \
--database-name=authorizer \
--database-host=db-host \
--database-port=5432
Key flags:
--database-type:postgres,mysql,planetscale,sqlite,sqlserver,mongodb,arangodb,yugabyte,mariadb,cassandradb,scylladb,couchbase,dynamodb, etc.--database-url: connection string.- Optional per-driver flags (name, host, port, TLS certs, etc.) -- see the mapping table in Migration v1 to v2.
Session / cache
./authorizer \
--redis-url=redis://user:pass@redis-host:6379/0
--redis-url: Redis connection string used for session storage. If omitted, sessions are stored in memory (suitable only for dev / single-node setups).
3. OAuth / app behavior
These flags replace v1 env such as CLIENT_ID, CLIENT_SECRET, and app behavior toggles.
./authorizer \
--client-id=YOUR_CLIENT_ID \
--client-secret=YOUR_CLIENT_SECRET \
--admin-secret=your-admin-secret \
--allowed-origins=https://your-app.com,http://localhost:3000 \
--default-authorize-response-type=code \
--default-authorize-response-mode=query
--client-id(required): instance/client identifier.--client-secret(required): secret used for token-related operations.--admin-secret(required, non-empty): super-admin secret for admin operations. Breaking change as of April 2026: there is no default any more — empty causes the server to exit at startup. Pick any non-empty value; the strength of the secret is your responsibility. See Security Hardening.--allowed-origins: comma-separated list of allowed origins (default*). A startup warning is logged when the value contains*— set an explicit allowlist for production. See CORS, CSRF and origin enforcement.--trusted-proxies(default empty): comma-separated CIDRs of reverse proxies whoseX-Forwarded-Forshould be honoured. Breaking change as of April 2026: defaults to none — operators behind a proxy must set this explicitly or rate limiting and audit logs will key on the proxy IP. See Trusted proxies.--url(default empty): canonical/trusted base URL of this instance (e.g.https://auth.example.com). When set, it is the only source used to build verification/reset/magic-link email URLs, the JWTissclaim, and OIDC discovery URLs — theX-Authorizer-URL,X-Forwarded-Host, andHostrequest headers are ignored for that purpose. Leaving it empty keeps legacy header-based derivation. Recommended for production, especially behind a reverse proxy. See Trusted base URL.
Organization / UI:
./authorizer \
--organization-name="Your Company" \
--organization-logo="https://your-cdn/logo.png" \
--enable-login-page=true \
--enable-playground=false \
--enable-graphql-introspection=false
--enable-login-page: set tofalseto disable the built-in login UI.--enable-playground: set tofalseto disable the GraphQL playground.--enable-graphql-introspection: set tofalsein hardened environments.
4. Auth behavior and cookies
Roles and auth flows
./authorizer \
--roles=user,admin \
--default-roles=user \
--protected-roles=admin \
--enable-strong-password=true \
--enable-basic-authentication=true \
--enable-email-verification=true \
--enable-magic-link-login=true \
--enable-signup=true
These replace v1 flags such as DISABLE_BASIC_AUTHENTICATION, DISABLE_EMAIL_VERIFICATION, etc.
See the Auth behavior mapping for exact correspondences.
--enable-email-verification requires SMTPStarting the server with --enable-email-verification=true and no working mail
path is now a fatal boot error. It used to start and fail silently per user.
Every route back into an unverified account terminates at the same mailbox, so without SMTP a user is created unverified and can never recover — and an unverified account also blocks a federated login for the same address. That is unrecoverable state, so it fails where an operator can see it rather than one user at a time.
A "working mail path" means all three of --smtp-host, --smtp-port (greater
than zero) and --smtp-sender-email. Setting only the host still fails.
Either configure SMTP or set --enable-email-verification=false.
Multi-factor authentication (MFA) & WebAuthn/passkeys
Breaking change: --enable-mfa, --enable-totp-login, --enable-email-otp,
and --enable-sms-otp are removed. TOTP, email OTP, SMS OTP, and
WebAuthn/passkey-as-MFA are now all on by default; opt out per method with
the --disable-* flags below. --enforce-mfa also flipped its default from
true to false — MFA is now optional and skippable unless you explicitly
enforce it.
./authorizer \
--enforce-mfa=false \
--disable-mfa=false \
--disable-totp-login=false \
--disable-webauthn-mfa=false \
--disable-email-otp=false \
--disable-sms-otp=false
--enforce-mfa(defaultfalse): require every user to complete MFA enrollment before a token is issued (mfaGateBlockEnroll, never skippable). Whenfalse, users are offered MFA setup once and may skip it (skip_mfa_setupmutation); a user's own already-enrolled factor is always required to verify regardless of this flag.--disable-mfa(defaultfalse): one-way global kill switch — forces MFA off entirely (TOTP/email-OTP/SMS-OTP unavailable,--enforce-mfaneutralized) regardless of the per-method flags below. Does not affect WebAuthn/passkey as a primary login method (only as an MFA factor).--disable-totp-login(defaultfalse): disable TOTP authenticator-app MFA.--disable-webauthn-mfa(defaultfalse): disable WebAuthn/passkey as an MFA factor. Does not affect WebAuthn/passkey as a primary, passwordless login method — that is always available and has no flag.--disable-email-otp(defaultfalse): disable email-OTP MFA. Only takes effect when SMTP is configured (--smtp-*); otherwise email OTP is unavailable regardless of this flag.--disable-sms-otp(defaultfalse): disable SMS-OTP MFA. Only takes effect when Twilio is configured (--twilio-*); otherwise SMS OTP is unavailable regardless of this flag.
Effective availability of each method is exposed on the public meta
GraphQL query (is_totp_mfa_enabled, is_email_otp_mfa_enabled,
is_sms_otp_mfa_enabled, is_webauthn_enabled, is_mfa_enforced) so
frontends never have to reverse-engineer flag combinations. See
MFA & Passkeys for
the full behavior — withheld-token first-time setup, lockout, admin recovery,
and the WebAuthn/passkey GraphQL operations.
Cookies
./authorizer \
--app-cookie-secure=true \
--admin-cookie-secure=true \
--app-cookie-same-site=none
--app-cookie-secure/--admin-cookie-secure(both defaulttrue): usetruefor HTTPS-only cookies in production.--app-cookie-same-site(defaultnone):SameSiteattribute for the session cookies — one oflax,strict, ornone.
The default of none is what allows an app on a different site to complete
a credentialed /session call at all. Set lax only when every app that reads
the session shares this host; it is the tighter setting, but it stops
cross-site session reads working.
An unrecognised value now exits at startup instead of silently falling back
to lax. That fallback was the dangerous part: an operator who asked for
strict and mistyped it got lax — a real downgrade from what they requested,
with nothing anywhere saying so. A mistyped none went the other way and
withheld the session cookie from cross-site apps, which presents as "login
randomly doesn't stick" with the cause three layers away.
strict is accepted, but note that one cookie deliberately ignores this
setting: the short-lived OAuth state cookie used during social login is
always Lax, or None when secure. A provider's callback arrives as a
cross-site redirect — a cross-site form_post for Apple — and Strict
withholds cookies on exactly those, so honouring strict there would break
every social login on the deployment.
5. JWT configuration
./authorizer \
--jwt-type=HS256 \
--jwt-secret=your-jwt-secret \
--jwt-role-claim=role
Or for asymmetric keys:
./authorizer \
--jwt-type=RS256 \
--jwt-private-key="$(cat /path/to/private.key)" \
--jwt-public-key="$(cat /path/to/public.key)" \
--encryption-key=your-encryption-key
Additional flags:
-
--custom-access-token-script: path/string for custom token augmentation logic (advanced use only). -
--refresh-token-expires-in(default2592000, 30 days): refresh-token lifetime in seconds. Previously hardcoded — now operator-configurable. -
--encryption-key(no default): the key used to encrypt secrets at rest — TOTP shared secrets and OTP digests. Separate from the JWT signing material.
In v2, the _generate_jwt_keys mutation is deprecated and returns an error; configure keys only via flags.
--encryption-keyAt-rest encryption no longer derives from --jwt-secret. A deployment using
an asymmetric JWT algorithm (RS*/ES*) with no --jwt-secret will refuse to
start until --encryption-key is set.
HMAC deployments (HS256/HS384/HS512) are unaffected: the key still falls
back to --jwt-secret, so no change is required.
This closes a vulnerability in 2.2.1 through 2.4.0-rc.13 where that fallback produced a publicly known constant for asymmetric deployments — read the security advisory before upgrading, as remediation includes forcing TOTP re-enrolment.
Note on key rotation:
--encryption-keyprotects TOTP shared secrets at rest and HMACs OTPs. Rotating it will lock out every user with an enrolled TOTP authenticator until they re-enrol. When it is unset it falls back to--jwt-secret, so on those deployments rotating the JWT secret rotates the at-rest key too. See OTP and TOTP at rest.
6. SMTP and SMS
SMTP
./authorizer \
--smtp-host=smtp.mailprovider.com \
--smtp-port=587 \
--smtp-username=user@example.com \
--smtp-password=strong-password \
--smtp-sender-email=auth@example.com \
--smtp-sender-name="Auth Team" \
--smtp-local-name=authorizer \
--smtp-skip-tls-verification=false
Twilio (SMS OTP)
./authorizer \
--twilio-account-sid=AC... \
--twilio-api-key=... \
--twilio-api-secret=... \
--twilio-sender=+123456789
7. Social / OAuth providers
Each provider uses its own set of flags:
./authorizer \
--google-client-id=... \
--google-client-secret=... \
--google-scopes="openid,email,profile" \
--github-client-id=... \
--github-client-secret=... \
--github-scopes="read:user,user:email"
Other supported providers follow the same pattern:
--facebook-client-id,--facebook-client-secret,--facebook-scopes--microsoft-client-id,--microsoft-client-secret,--microsoft-tenant-id,--microsoft-allowed-tenants,--microsoft-scopes--apple-client-id,--apple-client-secret,--apple-scopes--linkedin-client-id,--linkedin-client-secret,--linkedin-scopes--discord-client-id,--discord-client-secret,--discord-scopes--twitter-client-id,--twitter-client-secret,--twitter-scopes--twitch-client-id,--twitch-client-secret,--twitch-scopes--roblox-client-id,--roblox-client-secret,--roblox-scopes
Provider email attestation (2.4.0)
A social login whose provider does not attest the email address no longer signs the user into an existing account with that address.
OAuth proves the user holds an account at the provider. It proves nothing about
the email address that provider hands back — which is why OIDC has a separate
email_verified claim. Microsoft Entra is the practical case: on a multi-tenant
alias the email claim is mutable and unattested, so anyone able to set a
directory attribute in their own tenant could assert your address and be
logged into your account. This is the nOAuth
class of account takeover.
Signup with an unattested address still works — it just creates its own account instead of reaching an existing one.
Attestation comes from the provider's email_verified claim, or Entra's
xms_edov optional claim. To make Entra attest:
- pin
--microsoft-tenant-idto a single tenant, or - enable the
xms_edovoptional claim on the app registration, or - list the tenants you trust in
--microsoft-allowed-tenantswhen--microsoft-tenant-idis a multi-tenant alias (common,organizations,consumers). Empty means no restriction — and in that mode the tenant is untrusted, so the email it asserts will not link to an existing account.
--oauth-allow-unverified-provider-email=true is a temporary compatibility
escape hatch. It does not disable the check: an unattested address still
cannot cross into an account owned by another credential. It only re-permits
same-provider linking, which leaves two Entra tenants able to collide on one
address. The server logs a warning on every boot while it is set.
See Email verification contract for the per-provider signal table and the upgrade path.
8. Rate limiting
./authorizer \
--rate-limit-rps=30 \
--rate-limit-burst=20 \
--rate-limit-fail-closed=false
--rate-limit-rps: maximum sustained requests per second per IP (default30). Set to0to disable.--rate-limit-burst: maximum burst size per IP (default20).--rate-limit-fail-closed: whentrue, a failing rate-limit backend returns503instead of allowing the request (defaultfalse, fail-open).
Rate limiting is always enabled by default. When --redis-url is set, limits are shared across replicas via Redis. See Rate Limiting for full details.
9. Admin and GraphQL security flags
New in v2:
./authorizer \
--disable-admin-header-auth=true \
--enable-graphql-introspection=false \
--graphql-max-complexity=300 \
--graphql-max-depth=15 \
--graphql-max-aliases=30 \
--graphql-max-body-bytes=1048576
--disable-admin-header-auth: whentrue, the server ignoresX-Authorizer-Admin-Secretand only honors the secure admin cookie. Recommended for production.--enable-graphql-introspection: disable in locked-down environments.--graphql-max-complexity(default300): max total complexity score per operation.--graphql-max-depth(default15): max selection-set nesting depth.--graphql-max-aliases(default30): max aliased fields per operation (defends against alias amplification).--graphql-max-body-bytes(default1048576, 1 MiB): max GraphQL request body size.
GET /graphql is no longer accepted — clients must POST. Rejections are
counted in the authorizer_graphql_limit_rejections_total Prometheus
metric, labelled by limit kind. See
GraphQL hardening for details.
Authorization (FGA)
./authorizer \
--fga-store=postgres \
--fga-store-url="postgres://user:pass@host/db" \
--include-permissions-in-token=false \
--authorization-log-all-checks=false
--fga-store: backing store for the embedded OpenFGA engine — one ofsqlite,postgres,mysql, ormemory. Only needed when the main database is NoSQL (see paragraph below); for SQL main databases the engine reuses that database automatically.--fga-store-url: connection string for the FGA store when--fga-storeis set to a database driver.--fga-allow-unconstrained-agents(defaultfalse): see below.
--fga-allow-unconstrained-agents
A delegated (agent-acting-for-user) permission check evaluates
perms(agent) ∩ perms(user). If the authorization model declares no
type agent, the agent half cannot be evaluated at all.
As of 2.4.0 that check is denied. Previously it authorized as the delegating user alone, which silently handed the agent the user's full authority — the Confused Deputy the intersection exists to prevent. A security check that cannot be evaluated is not a check that passes.
Setting this flag to true restores the pre-2.4.0 behaviour while you migrate a
model. It does not make anything more permissive than 2.3.x was, but it does
mean agents carry their delegating user's full authority.
Every such request is logged at warn level and recorded as not_enforced on the
delegated-check metric, so the exposure shows up in dashboards rather than being
discovered during an incident.
The fix is to add type agent to your model — see
Agent identity. Grant your agents before
deploying the model, or their calls start being denied.
--include-permissions-in-token(defaultfalse): when true, the access token's claims include the caller's flat(resource, scope)grant list. Useful for stateless downstream services that don't want to round-trip back to Authorizer per check.--authorization-log-all-checks(defaultfalse): audit-log everyCheckPermissioncall, not just denials. Diagnostic; expensive at scale.
Authorizer ships an embedded OpenFGA (ReBAC) engine. It is enabled by default when the main database is SQL-compatible (SQLite/Postgres/MySQL) and reuses that database. For NoSQL main databases (MongoDB, DynamoDB, …) it is off unless you set --fga-store (one of sqlite/postgres/mysql/memory) and --fga-store-url. Checks fail closed. See Authorization (FGA).
9. Security headers
./authorizer \
--enable-hsts=true \
--disable-csp=false
--enable-hsts(defaultfalse): emitStrict-Transport-Security. Only enable behind TLS — turning HSTS on without TLS will lock browsers out for a year.--disable-csp(defaultfalse): disable the defaultContent-Security-Policyheader. CSP is on by default.
The defaults are conservative and documented at Security response headers.
10. Full security reference
See the dedicated Security Hardening page for:
- The complete list of security CLI flags introduced in April 2026
- Trusted-proxy configuration for various deployment topologies
- CSRF, CORS, OAuth flow, and webhook SSRF protections (all automatic)
- OTP and TOTP at-rest hardening, including the rolling-deploy note for multi-replica clusters
- Login error normalization and user-enumeration defences
- Multi-factor authentication (MFA) behavior, lockout, admin recovery, and WebAuthn/passkey ceremonies
11. Discovering all flags
To list all available flags and their defaults, run:
./authorizer --help
For a v1 to v2 mapping table, see Configuration Mapping.