diff --git a/CHANGELOG.md b/CHANGELOG.md index 0569eda2..3db18c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). @@ -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)).