From 87b56113ee34f984980ee5dee2565b449a4452d4 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sun, 12 Jul 2026 13:53:10 +0530 Subject: [PATCH] feat(mfa): add --disable-mfa one-way global kill switch Operators can turn MFA off entirely with a single flag instead of setting all three --disable-totp-login/--disable-email-otp/--disable-sms-otp. It is one-way: Finalize forces EnableMFA and EnforceMFA off when set, so it can never contradict the per-method flags the way the removed --enable-mfa did. WebAuthn/passkey is a separate login recipe and is unaffected. --- CHANGELOG.md | 2 +- cmd/root.go | 1 + internal/config/config.go | 14 ++++++++++++++ internal/config/config_finalize_test.go | 12 ++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2601627..0a7123d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING — MFA is on by default and optional per user.** MFA methods are now enabled by default and opted out via new `--disable-totp-login`, `--disable-email-otp`, and `--disable-sms-otp` 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. `--enforce-mfa` now defaults to `false` (MFA optional; users may skip enrollment and enable it later via profile edit) — set `--enforce-mfa` to make MFA mandatory. +- **BREAKING — MFA is on by default and optional per user.** MFA methods are now enabled by default and opted out via new `--disable-totp-login`, `--disable-email-otp`, and `--disable-sms-otp` 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. `--enforce-mfa` now defaults to `false` (MFA optional; users may skip enrollment and enable it later via profile edit) — set `--enforce-mfa` to make MFA mandatory. Use `--disable-mfa` as a one-way global kill switch to turn MFA off entirely regardless of the per-method flags (does not affect WebAuthn/passkey, which is a separate login recipe). - **License: relicensed from MIT to Apache License 2.0.** Per the CNCF IP Policy ([Charter §11(b)(iii)](https://github.com/cncf/foundation/blob/main/charter.md#11-ip-policy)), Authorizer's outbound code is now distributed under the Apache License 2.0. Existing copies distributed under the MIT License remain valid under their original grant; this change applies to the project's outbound license going forward. See [NOTICE](NOTICE) for attribution. - **Fine-grained authorization is always enforcing.** The previously-proposed `--authorization-enforcement` flag and its dual `permissive`/`enforcing` modes were removed before shipping. `required_permissions` checks against an unmatched or denied `(resource, scope)` pair return `unauthorized`. There is no permissive "log but allow" mode. - **Authz Prometheus shape**: `authorizer_authz_checks_total` has only a `result` label (`allowed|denied|unmatched|error`); `authorizer_authz_unmatched_total` has no labels. diff --git a/cmd/root.go b/cmd/root.go index 04a0c838..998df2d3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -190,6 +190,7 @@ func init() { f.BoolVar(&rootArgs.config.DisableTOTPLogin, "disable-totp-login", false, "Disable TOTP-based MFA (enabled by default)") f.BoolVar(&rootArgs.config.DisableEmailOTP, "disable-email-otp", false, "Disable email OTP MFA (enabled by default when email service is configured)") f.BoolVar(&rootArgs.config.DisableSMSOTP, "disable-sms-otp", false, "Disable SMS OTP MFA (enabled by default when SMS service is configured)") + f.BoolVar(&rootArgs.config.DisableMFA, "disable-mfa", false, "Globally disable MFA (TOTP/email/SMS OTP), overriding the per-method flags; does not affect WebAuthn/passkey") f.BoolVar(&rootArgs.config.EnableSignup, "enable-signup", true, "Enable signup") // Cookies flags diff --git a/internal/config/config.go b/internal/config/config.go index a20e643d..5681512b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -175,6 +175,12 @@ type Config struct { // DisableSMSOTP opts out of SMS OTP MFA (enabled by default when the SMS // service is configured). DisableSMSOTP bool + // DisableMFA is a one-way global kill switch: when set, Finalize forces + // EnableMFA and EnforceMFA off regardless of the per-method flags. It can + // only ever turn MFA off, so unlike the removed --enable-mfa it cannot + // contradict the per-method flags. Does not affect WebAuthn/passkey, which + // is a separate login recipe. + DisableMFA bool // EnableSignup boolean to enable signup EnableSignup bool // IsEmailServiceEnabled is derived from SMTP configurations @@ -392,4 +398,12 @@ func (c *Config) Finalize() { c.EnableMFA = c.EnableTOTPLogin || (c.EnableEmailOTP && c.IsEmailServiceEnabled) || (c.EnableSMSOTP && c.IsSMSServiceEnabled) + + // One-way global kill switch. Wins over everything: no MFA challenge is + // possible and enforcement is neutralized so signup cannot flag users for + // an MFA they can never complete. WebAuthn/passkey is unaffected. + if c.DisableMFA { + c.EnableMFA = false + c.EnforceMFA = false + } } diff --git a/internal/config/config_finalize_test.go b/internal/config/config_finalize_test.go index eef0205d..a67014e7 100644 --- a/internal/config/config_finalize_test.go +++ b/internal/config/config_finalize_test.go @@ -75,6 +75,18 @@ func TestFinalizeMFADerivation(t *testing.T) { wantSMS: false, wantMFA: false, }, + { + name: "DisableMFA kill switch forces MFA off despite usable methods", + setup: func(c *Config) { + c.DisableMFA = true + withSMTP(c) + withTwilio(c) + }, + wantTOTP: true, // per-method flags still derive true... + wantEmail: true, + wantSMS: true, + wantMFA: false, // ...but the kill switch forces overall MFA off + }, } for _, tt := range tests {