Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion backend/app/Filament/Ops/Pages/TwoFactorChallenge.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion backend/app/Http/Middleware/EnsureAdminTotpVerified.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +21 to 23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid caching a bypassed TOTP session

When production is run with OPS_ADMIN_TOTP_ENABLED=false, this disabled branch now stores ops_admin_totp_verified_user_id for the admin on ordinary Ops requests. If TOTP is later re-enabled while that same session is still alive, the required branch below treats the stale session value as a completed current-session challenge for enrolled admins, so they can continue into Ops without being challenged. Skip writing the verified marker when TOTP is disabled, or clear/version it when enforcement is re-enabled.

Useful? React with 👍 / 👎.


Expand Down
2 changes: 1 addition & 1 deletion backend/app/Http/Responses/Auth/OpsLoginResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
37 changes: 37 additions & 0 deletions backend/tests/Feature/Ops/AdminTotpEnforcementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down