fix(2fa): memoize challenge pending-user lookup; assert key-only view; tidy hint wording

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-14 21:17:18 +02:00
parent 55d513f241
commit aa43947dcf
4 changed files with 21 additions and 4 deletions

View File

@ -18,6 +18,21 @@ class TwoFactorChallenge extends Component
#[Validate('required|string')] #[Validate('required|string')]
public string $code = ''; public string $code = '';
private ?User $pendingUser = null;
private bool $pendingResolved = false;
/** The pending 2FA user, resolved once per request from the session. */
private function pendingUser(): ?User
{
if (! $this->pendingResolved) {
$this->pendingUser = User::find(session('2fa.user'));
$this->pendingResolved = true;
}
return $this->pendingUser;
}
public function mount() public function mount()
{ {
if (! session()->has('2fa.user')) { if (! session()->has('2fa.user')) {
@ -28,7 +43,7 @@ class TwoFactorChallenge extends Component
/** Whether the PENDING user has TOTP — the authenticator code field renders only then. */ /** Whether the PENDING user has TOTP — the authenticator code field renders only then. */
public function getPendingHasTotpProperty(): bool public function getPendingHasTotpProperty(): bool
{ {
return (bool) User::find(session('2fa.user'))?->hasTotp(); return (bool) $this->pendingUser()?->hasTotp();
} }
public function verify() public function verify()
@ -74,7 +89,7 @@ class TwoFactorChallenge extends Component
/** Whether to offer the security-key option for the pending user (domain+HTTPS + has a key). */ /** Whether to offer the security-key option for the pending user (domain+HTTPS + has a key). */
public function webauthnAvailable(): bool public function webauthnAvailable(): bool
{ {
$user = User::find(session('2fa.user')); $user = $this->pendingUser();
return $user !== null return $user !== null
&& app(WebauthnService::class)->available() && app(WebauthnService::class)->available()

View File

@ -77,7 +77,7 @@ return [
'challenge_key_subtitle' => 'Bestätige mit deinem Security-Key oder einem Backup-Code.', 'challenge_key_subtitle' => 'Bestätige mit deinem Security-Key oder einem Backup-Code.',
'challenge_backup_label' => 'Backup-Code', 'challenge_backup_label' => 'Backup-Code',
'challenge_backup_placeholder' => 'xxxxxxxxxx-xxxxxxxxxx', 'challenge_backup_placeholder' => 'xxxxxxxxxx-xxxxxxxxxx',
'challenge_backup_only_hint' => 'Kein Authenticator nötig — Security-Key oben oder ein Backup-Code.', 'challenge_backup_only_hint' => 'Kein Authenticator nötig — Security-Key oder ein Backup-Code.',
'recovery_heading' => 'Backup-Codes', 'recovery_heading' => 'Backup-Codes',
'recovery_subtitle' => 'Mit diesen Einmal-Codes meldest du dich an, wenn du keinen Authenticator hast.', 'recovery_subtitle' => 'Mit diesen Einmal-Codes meldest du dich an, wenn du keinen Authenticator hast.',
'recovery_warning' => 'Speichere diese Codes jetzt sicher ab — jeder Code funktioniert genau einmal. Lade sie herunter, solange sie angezeigt werden.', 'recovery_warning' => 'Speichere diese Codes jetzt sicher ab — jeder Code funktioniert genau einmal. Lade sie herunter, solange sie angezeigt werden.',

View File

@ -77,7 +77,7 @@ return [
'challenge_key_subtitle' => 'Confirm with your security key or a backup code.', 'challenge_key_subtitle' => 'Confirm with your security key or a backup code.',
'challenge_backup_label' => 'Backup code', 'challenge_backup_label' => 'Backup code',
'challenge_backup_placeholder' => 'xxxxxxxxxx-xxxxxxxxxx', 'challenge_backup_placeholder' => 'xxxxxxxxxx-xxxxxxxxxx',
'challenge_backup_only_hint' => 'No authenticator needed — use the security key above or a backup code.', 'challenge_backup_only_hint' => 'No authenticator needed — use a security key or a backup code.',
'recovery_heading' => 'Backup codes', 'recovery_heading' => 'Backup codes',
'recovery_subtitle' => 'Use these one-time codes to sign in when you do not have your authenticator.', 'recovery_subtitle' => 'Use these one-time codes to sign in when you do not have your authenticator.',
'recovery_warning' => 'Save these codes somewhere safe now — each works exactly once. Download them while they are shown.', 'recovery_warning' => 'Save these codes somewhere safe now — each works exactly once. Download them while they are shown.',

View File

@ -29,6 +29,8 @@ class ChallengeFactorAdaptTest extends TestCase
Livewire::test(TwoFactorChallenge::class) Livewire::test(TwoFactorChallenge::class)
->assertSet('pendingHasTotp', false) ->assertSet('pendingHasTotp', false)
->assertDontSee('000000')
->assertSee(__('auth.challenge_backup_label'))
->set('code', $codes[0]) ->set('code', $codes[0])
->call('verify') ->call('verify')
->assertRedirect(route('dashboard')); ->assertRedirect(route('dashboard'));