toBeFalse(); $user = User::factory()->create(); app(EnableTwoFactorAuthentication::class)($user); expect($user->toArray())->not->toHaveKeys(['two_factor_secret', 'two_factor_recovery_codes']); expect($user->two_factor_secret)->not->toBe(decrypt($user->two_factor_secret)); $this->get('/two-factor-challenge')->assertNotFound(); }); test('two factor foundation challenges a confirmed account and accepts a recovery code only once', function () { foundationMfaRoutes(); $user = User::factory()->create(); app(EnableTwoFactorAuthentication::class)($user); $user->forceFill(['two_factor_confirmed_at' => now()])->save(); $code = $user->recoveryCodes()[0]; $this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/two-factor-challenge'); $this->assertGuest(); $this->get('/two-factor-challenge')->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private') ->assertInertia(fn (Assert $p) => $p->component('auth/two-factor-challenge')->missing('secret')->missing('recoveryCodes')); $this->post('/two-factor-challenge', ['recovery_code' => $code])->assertRedirect(); $this->assertAuthenticatedAs($user); expect($user->fresh()->recoveryCodes())->not->toContain($code); $this->post('/logout'); $this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/two-factor-challenge'); $this->post('/two-factor-challenge', ['recovery_code' => $code])->assertSessionHasErrors(); $this->assertGuest(); }); test('two factor foundation rejects invalid codes and throttles repeated challenges', function () { foundationMfaRoutes(); $user = User::factory()->create(); app(EnableTwoFactorAuthentication::class)($user); $user->forceFill(['two_factor_confirmed_at' => now()])->save(); $this->withSession(['login.id' => $user->id]); for ($i = 0; $i < 5; $i++) { $this->postJson('/two-factor-challenge', ['recovery_code' => 'invalid'])->assertUnprocessable(); } $this->postJson('/two-factor-challenge', ['recovery_code' => 'invalid'])->assertStatus(429); $this->assertGuest(); }); test('two factor foundation accepts an authenticator code without exposing its secret', function () { foundationMfaRoutes(); $user = User::factory()->create(); app(EnableTwoFactorAuthentication::class)($user); $user->forceFill(['two_factor_confirmed_at' => now()])->save(); $this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/two-factor-challenge'); $code = (new Google2FA)->getCurrentOtp(decrypt($user->two_factor_secret)); $this->post('/two-factor-challenge', ['code' => $code])->assertRedirect(); $this->assertAuthenticatedAs($user); }); test('two factor foundation does not challenge an unfinished enrollment', function () { foundationMfaRoutes(); $user = User::factory()->create(); app(EnableTwoFactorAuthentication::class)($user); expect($user->hasEnabledTwoFactorAuthentication())->toBeFalse(); $this->post('/login', ['email' => $user->email, 'password' => 'password'])->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); }); test('two factor foundation password confirmation uses the native password check and private view', function () { $user = User::factory()->create(); $this->actingAs($user)->get('/user/confirm-password')->assertOk() ->assertInertia(fn (Assert $p) => $p->component('auth/confirm-password')); $this->post('/user/confirm-password', ['password' => 'wrong'])->assertSessionHasErrors('password'); $this->post('/user/confirm-password', ['password' => 'password'])->assertSessionHasNoErrors(); expect(session('auth.password_confirmed_at'))->not->toBeNull(); });