From 71b42ebb0fc89865a8660c99e85da1258e471b41 Mon Sep 17 00:00:00 2001 From: fermatmind <17551046983@163.com> Date: Sun, 12 Jul 2026 21:29:16 +0800 Subject: [PATCH] fix(ops): respect admin totp config in production --- AGENTS.md | 2 +- .../Filament/Ops/Pages/TwoFactorChallenge.php | 2 +- .../Middleware/EnsureAdminTotpVerified.php | 2 +- .../Http/Responses/Auth/OpsLoginResponse.php | 2 +- .../Feature/Ops/AdminTotpEnforcementTest.php | 37 +++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e1d80ec8f..7fe2fb772 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ Prefer a repo-compatible default implementation and mark options as optional. ### CMS Administrator MFA Discipline - When TOTP is required, an unenrolled administrator may access only enrollment, recovery/challenge, and logout surfaces; ordinary Ops and CMS routes fail closed. -- Production requires TOTP even if the environment toggle is misconfigured. Recovery codes are single-use, time-limited, and audited; their use requires credential rotation. +- TOTP is enabled by default and may be explicitly disabled through `OPS_ADMIN_TOTP_ENABLED=false`; production must honor that runtime configuration rather than force TOTP unconditionally. Recovery codes are single-use, time-limited, and audited; their use requires credential rotation. - Owner bootstrap passwords must use hidden interactive input and the configured strength policy; plaintext password command options are prohibited. ### DailyGiving Proof Handling Discipline diff --git a/backend/app/Filament/Ops/Pages/TwoFactorChallenge.php b/backend/app/Filament/Ops/Pages/TwoFactorChallenge.php index 169f185e7..6118289cf 100644 --- a/backend/app/Filament/Ops/Pages/TwoFactorChallenge.php +++ b/backend/app/Filament/Ops/Pages/TwoFactorChallenge.php @@ -28,7 +28,7 @@ public function verify(AdminTotpService $totp): void return; } - $totpRequired = (bool) config('admin.totp.enabled', true) || app()->environment('production'); + $totpRequired = (bool) config('admin.totp.enabled', true); if (! $totpRequired) { session(['ops_admin_totp_verified_user_id' => (int) $user->id]); $this->redirect('/ops/select-org', navigate: true); diff --git a/backend/app/Http/Middleware/EnsureAdminTotpVerified.php b/backend/app/Http/Middleware/EnsureAdminTotpVerified.php index 7128d5123..64373a00d 100644 --- a/backend/app/Http/Middleware/EnsureAdminTotpVerified.php +++ b/backend/app/Http/Middleware/EnsureAdminTotpVerified.php @@ -18,7 +18,7 @@ public function handle(Request $request, Closure $next): Response return $next($request); } - $totpRequired = (bool) config('admin.totp.enabled', true) || app()->environment('production'); + $totpRequired = (bool) config('admin.totp.enabled', true); if (! $totpRequired) { $request->session()->put('ops_admin_totp_verified_user_id', (int) $user->id); diff --git a/backend/app/Http/Responses/Auth/OpsLoginResponse.php b/backend/app/Http/Responses/Auth/OpsLoginResponse.php index 6bd529a25..720f63da0 100644 --- a/backend/app/Http/Responses/Auth/OpsLoginResponse.php +++ b/backend/app/Http/Responses/Auth/OpsLoginResponse.php @@ -21,7 +21,7 @@ public function toResponse($request): RedirectResponse|Redirector $guard = (string) config('admin.guard', 'admin'); $user = auth($guard)->user(); - $totpRequired = (bool) config('admin.totp.enabled', true) || app()->environment('production'); + $totpRequired = (bool) config('admin.totp.enabled', true); if ($user && $totpRequired && $user->totp_enabled_at === null) { return redirect()->to('/ops/two-factor-enrollment'); } diff --git a/backend/tests/Feature/Ops/AdminTotpEnforcementTest.php b/backend/tests/Feature/Ops/AdminTotpEnforcementTest.php index 10dba3243..829226f2f 100644 --- a/backend/tests/Feature/Ops/AdminTotpEnforcementTest.php +++ b/backend/tests/Feature/Ops/AdminTotpEnforcementTest.php @@ -4,10 +4,14 @@ namespace Tests\Feature\Ops; +use App\Http\Responses\Auth\OpsLoginResponse; use App\Models\AdminUser; use App\Models\AuditLog; use App\Services\Auth\AdminTotpService; +use Filament\Facades\Filament; +use Filament\PanelRegistry; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Tests\TestCase; @@ -44,6 +48,39 @@ public function test_enrolled_admin_requires_current_session_verification(): voi $this->assertNotSame(route('filament.ops.pages.two-factor-enrollment'), $response->headers->get('Location')); } + public function test_totp_can_be_disabled_in_production_by_configuration(): void + { + $this->app->detectEnvironment(static fn (): string => 'production'); + config()->set('admin.totp.enabled', false); + + $admin = $this->admin(['totp_enabled_at' => null, 'totp_secret' => null]); + + $response = $this->withSession(['ops_admin_totp_verified_user_id' => 0]) + ->actingAs($admin, (string) config('admin.guard', 'admin')) + ->get('/ops'); + + $this->assertNotSame(route('filament.ops.pages.two-factor-challenge'), $response->headers->get('Location')); + $this->assertNotSame(route('filament.ops.pages.two-factor-enrollment'), $response->headers->get('Location')); + $this->assertSame((int) $admin->id, session('ops_admin_totp_verified_user_id')); + } + + public function test_login_response_skips_totp_redirects_when_disabled_in_production(): void + { + $this->app->detectEnvironment(static fn (): string => 'production'); + config()->set('admin.totp.enabled', false); + Filament::setCurrentPanel(app(PanelRegistry::class)->get('ops')); + + $admin = $this->admin(['totp_enabled_at' => null, 'totp_secret' => null]); + $this->actingAs($admin, (string) config('admin.guard', 'admin')); + + $request = Request::create('/ops/login', 'POST'); + $request->setLaravelSession($this->app['session.store']); + + $response = (new OpsLoginResponse)->toResponse($request); + + $this->assertStringEndsWith('/ops/select-org', $response->getTargetUrl()); + } + public function test_recovery_code_is_time_limited_single_use_and_audited(): void { config()->set('admin.totp.recovery_ttl_days', 30);