Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Targets the 2.4.0 release. Significant additions include enterprise SSO (SAML Id

### Changed

- **BREAKING — `--url` is now required; the server refuses to start without it.** Every deployment must pass its own canonical address (e.g. `--url=https://auth.example.com`). Previously optional, in which case the server derived its own host from request headers. That fallback is a takeover primitive: an attacker sends a password-reset or email-verification request with a forged `Host`/`X-Forwarded-Host`, and the victim is emailed a **genuine** single-use link pointing at a domain the attacker controls — no prior access and no mailbox compromise needed (CWE-640). It also affects the JWT `iss` claim, magic-link URLs and OIDC discovery. Making the flag mandatory is the only fix that closes the class: validating the derived host against `--allowed-origins` would do nothing on the default `*`, which is the configuration the attack targets. An unusable value is rejected rather than silently ignored — `--url=auth.example.com` (no scheme) or a URL carrying user info would otherwise start in the vulnerable configuration while looking configured. This costs no supported capability: setting `--url` already collapsed an instance to a single canonical host, so multi-host operation only ever worked on the vulnerable path (verified org domains are email-domain-to-organization routing, not HTTP virtual hosting, and are unaffected). **Note `--url` is not `--allowed-origins`** — the first is this server's own address, the second is the apps it may redirect to; you need both ([#764](https://github.com/authorizerdev/authorizer/pull/764)).
- **BREAKING — at-rest encryption key split from the JWT secret (`--encryption-key`).** The key used to encrypt secrets at rest (TOTP secrets, and the OTP digests behind email/SMS verification and password reset) is now its own input and no longer derives from `--jwt-secret`. **A deployment using RS256/ES256 (`--jwt-private-key`/`--jwt-public-key`) without `--jwt-secret` will refuse to start until `--encryption-key` is set** — HMAC deployments (HS256/384/512) are unaffected, as the JWT secret still resolves the key. This is a security fix, not a preference: in **2.2.1 through 2.4.0-rc.13**, an asymmetric-JWT deployment with no `--jwt-secret` silently fell back to a **public constant** compiled into the source, so anything encrypted at rest was protected by a key any reader of the repository already had. Operators on those versions must treat existing TOTP enrollments as compromised: rotate `--encryption-key`, then have affected users re-enroll (existing ciphertext was written under the old key and will not decrypt). Recovery codes are unaffected by the rotation — they are stored as unkeyed SHA-256 digests. HMAC deployments that leave `--encryption-key` unset now log a startup warning: under the fallback, rotating `--jwt-secret` also changes the at-rest key, and there is no re-encryption path — every enrolled TOTP user is locked out. Set a distinct `--encryption-key` before rotating. There is no `ENCRYPTION_KEY` environment variable — v2 is flag-only ([#742](https://github.com/authorizerdev/authorizer/pull/742)).
- **Admin dashboard UI migration from Chakra UI to shadcn/ui + Tailwind CSS**: Dashboard (`web/dashboard/`) completely modernized. Replaced Chakra UI v2 with shadcn/ui (Radix primitives) + Tailwind CSS v4. All TypeScript `any` types and `@ts-ignore` directives eliminated; full type safety on GraphQL responses, component props, and data models. Dead dependencies removed (react-draft-wysiwyg, @emotion, framer-motion, react-icons, focus-visible). 17 shadcn/ui-style components built on Radix; Authorizer branding (logo + blue-500) applied throughout. Cleaner tables, Sheet panels for forms, sonner toast notifications, skeleton loading states ([#605](https://github.com/authorizerdev/authorizer/pull/605)).
- **BREAKING — MFA behavior completely redesigned: on by default, optional per user, withheld token until setup complete.** MFA methods (TOTP, Email OTP, SMS OTP, WebAuthn) are now enabled by default and opted out via new `--disable-totp-login`, `--disable-email-otp`, `--disable-sms-otp`, and `--disable-webauthn-mfa` flags; the old `--enable-totp-login`, `--enable-mfa`, `--enable-email-otp`, and `--enable-sms-otp` flags are removed. Email and SMS OTP only take effect when their provider (SMTP / Twilio) is configured. Whether MFA is available is now derived from the enabled methods rather than a standalone flag, which fixes the case where MFA appeared "enabled" while every method was unavailable. **New token-withholding behavior:** when MFA is optional (`--enforce-mfa` default `false`), first-time users who haven't set up MFA no longer receive an immediate token followed by a setup offer — the token is withheld until the user completes enrollment or explicitly skips (remembered as `has_skipped_mfa_setup_at`). This withheld-token model now applies uniformly to password login, passkey login, signup, and social login. When `--enforce-mfa` is set, MFA is mandatory and un-skippable. **Email/SMS OTP now require explicit enrollment** (new `email_otp_mfa_setup`/`sms_otp_mfa_setup` mutations) before they can be used for MFA verification, fixing the previous behavior where they fired automatically for any user with a phone/email on file. **Admin recovery:** new `reset_mfa` operation on `_update_user` clears all MFA state and enrolled factors across all storage backends. **User-initiated lockout:** new `lock_mfa` mutation prevents future MFA enrollment (admin-recoverable); lockout is refused if a verified Email/SMS OTP factor exists as a fallback. **`--disable-mfa` one-way kill switch** disables MFA entirely regardless of per-method flags (does not affect WebAuthn, which is a separate login recipe) ([#682](https://github.com/authorizerdev/authorizer/pull/682), [#684](https://github.com/authorizerdev/authorizer/pull/684), [#685](https://github.com/authorizerdev/authorizer/pull/685), [#686](https://github.com/authorizerdev/authorizer/pull/686)).
Expand Down Expand Up @@ -84,7 +85,7 @@ Targets the 2.4.0 release. Significant additions include enterprise SSO (SAML Id
- **Session revocation on password reset**: when a user resets their password via email verification link or the recovery flow, all of their active sessions are immediately revoked, preventing unauthorized account access after a compromised password ([#669](https://github.com/authorizerdev/authorizer/pull/669), [#673](https://github.com/authorizerdev/authorizer/pull/673)).
- **Per-user TOTP brute-force lockout**: failed TOTP verification attempts are now tracked per user with temporary lockout after 5 failures (matching the existing per-user email/SMS OTP lockout), and recovery codes are hashed at rest using bcrypt (never stored plaintext) ([#670](https://github.com/authorizerdev/authorizer/pull/670)).
- **SAML ACS CSRF-origin exemption**: the SAML Assertion Consumer Service endpoint is now correctly exempted from strict CSRF Origin checking, since browser-based SAML POSTs from a different origin (the IdP) are expected and legitimate ([#666](https://github.com/authorizerdev/authorizer/pull/666)).
- **Trusted base URL + email/SMS OTP lockout**: new `--url` flag (`config.AuthorizerURL`) sets the single trusted source for the server's own URL used in email verification links, JWT `iss` claim, and OIDC discovery, preventing header-spoofing attacks that could redirect users to attacker-controlled sites while carrying single-use tokens. Email/SMS OTP verification now gets the same per-user brute-force lockout that TOTP already had ([#698](https://github.com/authorizerdev/authorizer/pull/698)).
- **Trusted base URL + email/SMS OTP lockout**: new `--url` flag (`config.AuthorizerURL`) sets the single trusted source for the server's own URL used in email verification links, JWT `iss` claim, and OIDC discovery, preventing header-spoofing attacks that could redirect users to attacker-controlled sites while carrying single-use tokens. **The flag is mandatory as of 2.4.0** — leaving it unset kept the header-derived fallback, so the fix only lands when it is set; see the BREAKING entry under Changed. Email/SMS OTP verification now gets the same per-user brute-force lockout that TOTP already had ([#698](https://github.com/authorizerdev/authorizer/pull/698)).
- **Type-safe error handling in gRPC admin service**: admin service methods now return properly-typed errors (400 for validation, 409 for conflicts, etc.) instead of generic Internal errors (500), and public-method bypass is tightly scoped to only the public service and `AdminLogin` ([#700](https://github.com/authorizerdev/authorizer/pull/700)).
- **Atomic storage operations with transaction guards**: `UpdateUsers` empty-ids filter is now enforced across all 13 database providers (preventing silent full-table updates on Mongo/Arango/Cassandra/Couchbase/DynamoDB); cascade deletes (`DeleteOrganization`, `DeleteClient`, `DeleteWebhook`, `DeleteUser`) are now wrapped in transactions, rolling back on partial failure ([#699](https://github.com/authorizerdev/authorizer/pull/699)).
- **Delegated tokens are gated per operation by their `scope` claim**: an RFC 8693 delegated token may only reach operations cleared for delegated callers, and only while its `scope` carries the scope that operation requires. Refusals are `insufficient_scope` (RFC 6750 §3.1). Until this existed the attenuation the token endpoint computes — `subject_token.scope ∩ agent.allowed_scopes` — was returned to the caller and then never consulted, so a delegated token reached **every** first-party operation: an agent an operator granted `openid` for a downstream MCP server could read the delegating user's profile, mutate the account, and deactivate it. Read-only identity and permission queries (`check_permissions`, `list_permissions`, `profile`, `meta` — exactly the built-in MCP tool set) require only `openid` and are unaffected. Mutating operations require a scope no client requests by default: `authorizer:profile:write` for `update_profile`, `authorizer:account:delete` for `deactivate_account`. Because a delegated scope is the intersection of the user's and the agent's, a sensitive operation needs **both** halves — the user's own token must carry the scope and an admin must have granted the agent a ceiling including it — so neither party can widen an agent alone. **Fails closed**: any operation not explicitly cleared is denied to delegated callers whatever scope they hold, so new operations are unreachable by agents until deliberately added. **First-party tokens are deliberately not gated** — `login` accepts a caller-supplied `scope` with no allow-list, making it a hint rather than a boundary, so enforcing it there would break existing clients while granting no security. Enforced identically on GraphQL and on gRPC (which also covers the REST gateway and the MCP server) ([#742](https://github.com/authorizerdev/authorizer/pull/742)).
Expand Down