]> BookStack Code Mirror - bookstack/blobdiff - tests/Auth/MfaVerificationTest.php
Added "page_include_parse" theme event
[bookstack] / tests / Auth / MfaVerificationTest.php
index 3f272cffbcb043360429041689651518df9e9c2c..ba4c9b983a3feebce56e44c34cb5ca8904030d20 100644 (file)
@@ -2,9 +2,12 @@
 
 namespace Tests\Auth;
 
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\Mfa\MfaValue;
 use BookStack\Auth\Access\Mfa\TotpService;
+use BookStack\Auth\Role;
 use BookStack\Auth\User;
+use BookStack\Exceptions\StoppedAuthenticationException;
 use Illuminate\Support\Facades\Hash;
 use PragmaRX\Google2FA\Google2FA;
 use Tests\TestCase;
@@ -20,10 +23,10 @@ class MfaVerificationTest extends TestCase
         $resp = $this->get('/mfa/verify');
         $resp->assertSee('Verify Access');
         $resp->assertSee('Enter the code, generated using your mobile app, below:');
-        $resp->assertElementExists('form[action$="/mfa/verify/totp"] input[name="code"]');
+        $this->withHtml($resp)->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"][autofocus]');
 
         $google2fa = new Google2FA();
-        $resp = $this->post('/mfa/verify/totp', [
+        $resp = $this->post('/mfa/totp/verify', [
             'code' => $google2fa->getCurrentOtp($secret),
         ]);
         $resp->assertRedirect('/');
@@ -35,7 +38,7 @@ class MfaVerificationTest extends TestCase
         [$user, $secret, $loginResp] = $this->startTotpLogin();
 
         $resp = $this->get('/mfa/verify');
-        $resp = $this->post('/mfa/verify/totp', [
+        $resp = $this->post('/mfa/totp/verify', [
             'code' => '',
         ]);
         $resp->assertRedirect('/mfa/verify');
@@ -44,7 +47,7 @@ class MfaVerificationTest extends TestCase
         $resp->assertSeeText('The code field is required.');
         $this->assertNull(auth()->user());
 
-        $resp = $this->post('/mfa/verify/totp', [
+        $resp = $this->post('/mfa/totp/verify', [
             'code' => '123321',
         ]);
         $resp->assertRedirect('/mfa/verify');
@@ -63,9 +66,9 @@ class MfaVerificationTest extends TestCase
         $resp->assertSee('Verify Access');
         $resp->assertSee('Backup Code');
         $resp->assertSee('Enter one of your remaining backup codes below:');
-        $resp->assertElementExists('form[action$="/mfa/verify/backup_codes"] input[name="code"]');
+        $this->withHtml($resp)->assertElementExists('form[action$="/mfa/backup_codes/verify"] input[name="code"]');
 
-        $resp = $this->post('/mfa/verify/backup_codes', [
+        $resp = $this->post('/mfa/backup_codes/verify', [
             'code' => $codes[1],
         ]);
 
@@ -82,7 +85,7 @@ class MfaVerificationTest extends TestCase
         [$user, $codes, $loginResp] = $this->startBackupCodeLogin();
 
         $resp = $this->get('/mfa/verify');
-        $resp = $this->post('/mfa/verify/backup_codes', [
+        $resp = $this->post('/mfa/backup_codes/verify', [
             'code' => '',
         ]);
         $resp->assertRedirect('/mfa/verify');
@@ -91,7 +94,7 @@ class MfaVerificationTest extends TestCase
         $resp->assertSeeText('The code field is required.');
         $this->assertNull(auth()->user());
 
-        $resp = $this->post('/mfa/verify/backup_codes', [
+        $resp = $this->post('/mfa/backup_codes/verify', [
             'code' => 'ab123-ab456',
         ]);
         $resp->assertRedirect('/mfa/verify');
@@ -105,7 +108,7 @@ class MfaVerificationTest extends TestCase
     {
         [$user, $codes, $loginResp] = $this->startBackupCodeLogin();
 
-        $this->post('/mfa/verify/backup_codes', [
+        $this->post('/mfa/backup_codes/verify', [
             'code' => $codes[0],
         ]);
         $this->assertNotNull(auth()->user());
@@ -114,7 +117,7 @@ class MfaVerificationTest extends TestCase
 
         $this->post('/login', ['email' => $user->email, 'password' => 'password']);
         $this->get('/mfa/verify');
-        $resp = $this->post('/mfa/verify/backup_codes', [
+        $resp = $this->post('/mfa/backup_codes/verify', [
             'code' => $codes[0],
         ]);
         $resp->assertRedirect('/mfa/verify');
@@ -128,7 +131,7 @@ class MfaVerificationTest extends TestCase
     {
         [$user, $codes, $loginResp] = $this->startBackupCodeLogin(['abc12-def45', 'abc12-def46']);
 
-        $resp = $this->post('/mfa/verify/backup_codes', [
+        $resp = $this->post('/mfa/backup_codes/verify', [
             'code' => $codes[0],
         ]);
         $resp = $this->followRedirects($resp);
@@ -146,24 +149,110 @@ class MfaVerificationTest extends TestCase
 
         /** @var TestResponse $mfaView */
         $mfaView = $this->followingRedirects()->post('/login', [
-            'email' => $user->email,
+            'email'    => $user->email,
             'password' => 'password',
         ]);
 
         // Totp shown by default
-        $mfaView->assertElementExists('form[action$="/mfa/verify/totp"] input[name="code"]');
-        $mfaView->assertElementContains('a[href$="/mfa/verify?method=backup_codes"]', 'Verify using a backup code');
+        $this->withHtml($mfaView)->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]');
+        $this->withHtml($mfaView)->assertElementContains('a[href$="/mfa/verify?method=backup_codes"]', 'Verify using a backup code');
 
         // Ensure can view backup_codes view
         $resp = $this->get('/mfa/verify?method=backup_codes');
-        $resp->assertElementExists('form[action$="/mfa/verify/backup_codes"] input[name="code"]');
-        $resp->assertElementContains('a[href$="/mfa/verify?method=totp"]', 'Verify using a mobile app');
+        $this->withHtml($resp)->assertElementExists('form[action$="/mfa/backup_codes/verify"] input[name="code"]');
+        $this->withHtml($resp)->assertElementContains('a[href$="/mfa/verify?method=totp"]', 'Verify using a mobile app');
     }
 
-    //    TODO !! - Test no-existing MFA
+    public function test_mfa_required_with_no_methods_leads_to_setup()
+    {
+        $user = $this->getEditor();
+        $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->getEditor();
+        /** @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());
+    }
 
     /**
-     * @return Array<User, string, TestResponse>
+     * @return array<User, string, TestResponse>
      */
     protected function startTotpLogin(): array
     {
@@ -173,7 +262,7 @@ class MfaVerificationTest extends TestCase
         $user->save();
         MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret);
         $loginResp = $this->post('/login', [
-            'email' => $user->email,
+            'email'    => $user->email,
             'password' => 'password',
         ]);
 
@@ -181,20 +270,19 @@ class MfaVerificationTest extends TestCase
     }
 
     /**
-     * @return Array<User, string, TestResponse>
+     * @return array<User, string, TestResponse>
      */
-    protected function startBackupCodeLogin($codes = ['kzzu6-1pgll','bzxnf-plygd','bwdsp-ysl51','1vo93-ioy7n','lf7nw-wdyka','xmtrd-oplac']): array
+    protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array
     {
         $user = $this->getEditor();
         $user->password = Hash::make('password');
         $user->save();
         MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes));
         $loginResp = $this->post('/login', [
-            'email' => $user->email,
+            'email'    => $user->email,
             'password' => 'password',
         ]);
 
         return [$user, $codes, $loginResp];
     }
-
-}
\ No newline at end of file
+}