]> BookStack Code Mirror - bookstack/blob - tests/Auth/MfaConfigurationTest.php
Merge pull request #5917 from BookStackApp/copy_references
[bookstack] / tests / Auth / MfaConfigurationTest.php
1 <?php
2
3 namespace Tests\Auth;
4
5 use BookStack\Access\Mfa\MfaValue;
6 use BookStack\Activity\ActivityType;
7 use BookStack\Users\Models\Role;
8 use BookStack\Users\Models\User;
9 use Illuminate\Support\Facades\Hash;
10 use PragmaRX\Google2FA\Google2FA;
11 use Tests\TestCase;
12
13 class MfaConfigurationTest extends TestCase
14 {
15     public function test_totp_setup()
16     {
17         $editor = $this->users->editor();
18         $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
19
20         // Setup page state
21         $resp = $this->actingAs($editor)->get('/mfa/setup');
22         $this->withHtml($resp)->assertElementContains('a[href$="/mfa/totp/generate"]', 'Setup');
23
24         // Generate page access
25         $resp = $this->get('/mfa/totp/generate');
26         $resp->assertSee('Mobile App Setup');
27         $resp->assertSee('Verify Setup');
28         $this->withHtml($resp)->assertElementExists('form[action$="/mfa/totp/confirm"] button');
29         $this->assertSessionHas('mfa-setup-totp-secret');
30         $svg = $this->withHtml($resp)->getOuterHtml('#main-content .card svg');
31
32         // Validation error, code should remain the same
33         $resp = $this->post('/mfa/totp/confirm', [
34             'code' => 'abc123',
35         ]);
36         $resp->assertRedirect('/mfa/totp/generate');
37         $resp = $this->followRedirects($resp);
38         $resp->assertSee('The provided code is not valid or has expired.');
39         $revisitSvg = $this->withHtml($resp)->getOuterHtml('#main-content .card svg');
40         $this->assertTrue($svg === $revisitSvg);
41         $secret = decrypt(session()->get('mfa-setup-totp-secret'));
42
43         $resp->assertSee("?secret={$secret}&issuer=BookStack&algorithm=SHA1&digits=6&period=30");
44
45         // Successful confirmation
46         $google2fa = new Google2FA();
47         $otp = $google2fa->getCurrentOtp($secret);
48         $resp = $this->post('/mfa/totp/confirm', [
49             'code' => $otp,
50         ]);
51         $resp->assertRedirect('/mfa/setup');
52
53         // Confirmation of setup
54         $resp = $this->followRedirects($resp);
55         $resp->assertSee('Multi-factor method successfully configured');
56         $this->withHtml($resp)->assertElementContains('a[href$="/mfa/totp/generate"]', 'Reconfigure');
57
58         $this->assertDatabaseHas('mfa_values', [
59             'user_id' => $editor->id,
60             'method'  => 'totp',
61         ]);
62         $this->assertFalse(session()->has('mfa-setup-totp-secret'));
63         $value = MfaValue::query()->where('user_id', '=', $editor->id)
64             ->where('method', '=', 'totp')->first();
65         $this->assertEquals($secret, decrypt($value->value));
66     }
67
68     public function test_backup_codes_setup()
69     {
70         $editor = $this->users->editor();
71         $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
72
73         // Setup page state
74         $resp = $this->actingAs($editor)->get('/mfa/setup');
75         $this->withHtml($resp)->assertElementContains('a[href$="/mfa/backup_codes/generate"]', 'Setup');
76
77         // Generate page access
78         $resp = $this->get('/mfa/backup_codes/generate');
79         $resp->assertSee('Backup Codes');
80         $this->withHtml($resp)->assertElementContains('form[action$="/mfa/backup_codes/confirm"]', 'Confirm and Enable');
81         $this->assertSessionHas('mfa-setup-backup-codes');
82         $codes = decrypt(session()->get('mfa-setup-backup-codes'));
83         // Check code format
84         $this->assertCount(16, $codes);
85         $this->assertEquals(16 * 11, strlen(implode('', $codes)));
86         // Check download link
87         $resp->assertSee(base64_encode(implode("\n\n", $codes)));
88
89         // Confirm submit
90         $resp = $this->post('/mfa/backup_codes/confirm');
91         $resp->assertRedirect('/mfa/setup');
92
93         // Confirmation of setup
94         $resp = $this->followRedirects($resp);
95         $resp->assertSee('Multi-factor method successfully configured');
96         $this->withHtml($resp)->assertElementContains('a[href$="/mfa/backup_codes/generate"]', 'Reconfigure');
97
98         $this->assertDatabaseHas('mfa_values', [
99             'user_id' => $editor->id,
100             'method'  => 'backup_codes',
101         ]);
102         $this->assertFalse(session()->has('mfa-setup-backup-codes'));
103         $value = MfaValue::query()->where('user_id', '=', $editor->id)
104             ->where('method', '=', 'backup_codes')->first();
105         $this->assertEquals($codes, json_decode(decrypt($value->value)));
106     }
107
108     public function test_backup_codes_cannot_be_confirmed_if_not_previously_generated()
109     {
110         $resp = $this->asEditor()->post('/mfa/backup_codes/confirm');
111         $resp->assertStatus(500);
112     }
113
114     public function test_mfa_method_count_is_visible_on_user_edit_page()
115     {
116         $user = $this->users->editor();
117         $resp = $this->actingAs($this->users->admin())->get($user->getEditUrl());
118         $resp->assertSee('0 methods configured');
119
120         MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');
121         $resp = $this->get($user->getEditUrl());
122         $resp->assertSee('1 method configured');
123
124         MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, 'test');
125         $resp = $this->get($user->getEditUrl());
126         $resp->assertSee('2 methods configured');
127     }
128
129     public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page()
130     {
131         $admin = $this->users->admin();
132         $resp = $this->actingAs($admin)->get($admin->getEditUrl());
133         $this->withHtml($resp)->assertElementExists('a[href$="/mfa/setup"]');
134
135         $resp = $this->actingAs($admin)->get($this->users->editor()->getEditUrl());
136         $this->withHtml($resp)->assertElementNotExists('a[href$="/mfa/setup"]');
137     }
138
139     public function test_mfa_indicator_shows_in_user_list()
140     {
141         $admin = $this->users->admin();
142         User::query()->where('id', '!=', $admin->id)->delete();
143
144         $resp = $this->actingAs($admin)->get('/settings/users');
145         $this->withHtml($resp)->assertElementNotExists('[title="MFA Configured"] svg');
146
147         MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');
148         $resp = $this->actingAs($admin)->get('/settings/users');
149         $this->withHtml($resp)->assertElementExists('[title="MFA Configured"] svg');
150     }
151
152     public function test_remove_mfa_method()
153     {
154         $admin = $this->users->admin();
155
156         MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');
157         $this->assertEquals(1, $admin->mfaValues()->count());
158         $resp = $this->actingAs($admin)->get('/mfa/setup');
159         $this->withHtml($resp)->assertElementExists('form[action$="/mfa/totp/remove"]');
160
161         $resp = $this->delete('/mfa/totp/remove');
162         $resp->assertRedirect('/mfa/setup');
163         $resp = $this->followRedirects($resp);
164         $resp->assertSee('Multi-factor method successfully removed');
165
166         $this->assertActivityExists(ActivityType::MFA_REMOVE_METHOD);
167         $this->assertEquals(0, $admin->mfaValues()->count());
168     }
169
170     public function test_mfa_required_if_set_on_role()
171     {
172         $user = $this->users->viewer();
173         $user->password = Hash::make('password');
174         $user->save();
175         /** @var Role $role */
176         $role = $user->roles()->first();
177         $role->mfa_enforced = true;
178         $role->save();
179
180         $resp = $this->post('/login', ['email' => $user->email, 'password' => 'password']);
181         $this->assertFalse(auth()->check());
182         $resp->assertRedirect('/mfa/verify');
183     }
184
185     public function test_mfa_required_if_mfa_option_configured()
186     {
187         $user = $this->users->viewer();
188         $user->password = Hash::make('password');
189         $user->save();
190         $user->mfaValues()->create([
191             'method' => MfaValue::METHOD_TOTP,
192             'value'  => 'test',
193         ]);
194
195         $resp = $this->post('/login', ['email' => $user->email, 'password' => 'password']);
196         $this->assertFalse(auth()->check());
197         $resp->assertRedirect('/mfa/verify');
198     }
199
200     public function test_totp_setup_url_shows_correct_user_when_setup_forced_upon_login()
201     {
202         $admin = $this->users->admin();
203         /** @var Role $role */
204         $role = $admin->roles()->first();
205         $role->mfa_enforced = true;
206         $role->save();
207
208         $resp = $this->post('/login', ['email' => $admin->email, 'password' => 'password']);
209         $this->assertFalse(auth()->check());
210         $resp->assertRedirect('/mfa/verify');
211
212         $resp = $this->get('/mfa/totp/generate');
213         $resp->assertSeeText('Mobile App Setup');
214         $resp->assertDontSee('otpauth://totp/BookStack:guest%40example.com', false);
215         $resp->assertSee('otpauth://totp/BookStack:admin%40admin.com', false);
216     }
217 }