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
parent
55d513f241
commit
aa43947dcf
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue