+ public function test_mfa_required_with_no_methods_leads_to_setup()
+ {
+ $user = $this->users->editor();
+ $user->password = Hash::make('password');
+ $user->save();
+ /** @var Role $role */
+ $role = $user->roles->first();
+ $role->mfa_enforced = true;
+ $role->save();
+
+ $this->assertDatabaseMissing('mfa_values', [
+ 'user_id' => $user->id,
+ ]);
+
+ /** @var TestResponse $resp */
+ $resp = $this->followingRedirects()->post('/login', [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+
+ $resp->assertSeeText('No Methods Configured');
+ $this->withHtml($resp)->assertElementContains('a[href$="/mfa/setup"]', 'Configure');
+
+ $this->get('/mfa/backup_codes/generate');
+ $resp = $this->post('/mfa/backup_codes/confirm');
+ $resp->assertRedirect('/login');
+ $this->assertDatabaseHas('mfa_values', [
+ 'user_id' => $user->id,
+ ]);
+
+ $resp = $this->get('/login');
+ $resp->assertSeeText('Multi-factor method configured, Please now login again using the configured method.');
+
+ $resp = $this->followingRedirects()->post('/login', [
+ 'email' => $user->email,
+ 'password' => 'password',
+ ]);
+ $resp->assertSeeText('Enter one of your remaining backup codes below:');
+ }
+
+ public function test_mfa_setup_route_access()
+ {
+ $routes = [
+ ['get', '/mfa/setup'],
+ ['get', '/mfa/totp/generate'],
+ ['post', '/mfa/totp/confirm'],
+ ['get', '/mfa/backup_codes/generate'],
+ ['post', '/mfa/backup_codes/confirm'],
+ ];
+
+ // Non-auth access
+ foreach ($routes as [$method, $path]) {
+ $resp = $this->call($method, $path);
+ $resp->assertRedirect('/login');
+ }
+
+ // Attempted login user, who has configured mfa, access
+ // Sets up user that has MFA required after attempted login.
+ $loginService = $this->app->make(LoginService::class);
+ $user = $this->users->editor();
+ /** @var Role $role */
+ $role = $user->roles->first();
+ $role->mfa_enforced = true;
+ $role->save();
+
+ try {
+ $loginService->login($user, 'testing');
+ } catch (StoppedAuthenticationException $e) {
+ }
+ $this->assertNotNull($loginService->getLastLoginAttemptUser());
+
+ MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '[]');
+ foreach ($routes as [$method, $path]) {
+ $resp = $this->call($method, $path);
+ $resp->assertRedirect('/login');
+ }
+ }
+
+ public function test_login_mfa_interception_does_not_log_error()
+ {
+ $logHandler = $this->withTestLogger();
+
+ [$user, $secret, $loginResp] = $this->startTotpLogin();
+
+ $loginResp->assertRedirect('/mfa/verify');
+ $this->assertFalse($logHandler->hasErrorRecords());
+ }