]> BookStack Code Mirror - bookstack/blob - tests/Auth/MfaConfigurationTest.php
f332b6721d56d16e132b6506ef31a87b9e0da0f8
[bookstack] / tests / Auth / MfaConfigurationTest.php
1 <?php
2
3 namespace Tests\Auth;
4
5 use BookStack\Auth\Access\Mfa\MfaValue;
6 use PragmaRX\Google2FA\Google2FA;
7 use Tests\TestCase;
8
9 class MfaConfigurationTest extends TestCase
10 {
11
12     public function test_totp_setup()
13     {
14         $editor = $this->getEditor();
15         $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
16
17         // Setup page state
18         $resp = $this->actingAs($editor)->get('/mfa/setup');
19         $resp->assertElementContains('a[href$="/mfa/totp-generate"]', 'Setup');
20
21         // Generate page access
22         $resp = $this->get('/mfa/totp-generate');
23         $resp->assertSee('Mobile App Setup');
24         $resp->assertSee('Verify Setup');
25         $resp->assertElementExists('form[action$="/mfa/totp-confirm"] button');
26         $this->assertSessionHas('mfa-setup-totp-secret');
27         $svg = $resp->getElementHtml('#main-content .card svg');
28
29         // Validation error, code should remain the same
30         $resp = $this->post('/mfa/totp-confirm', [
31             'code' => 'abc123',
32         ]);
33         $resp->assertRedirect('/mfa/totp-generate');
34         $resp = $this->followRedirects($resp);
35         $resp->assertSee('The provided code is not valid or has expired.');
36         $revisitSvg = $resp->getElementHtml('#main-content .card svg');
37         $this->assertTrue($svg === $revisitSvg);
38
39         // Successful confirmation
40         $google2fa = new Google2FA();
41         $secret = decrypt(session()->get('mfa-setup-totp-secret'));
42         $otp = $google2fa->getCurrentOtp($secret);
43         $resp = $this->post('/mfa/totp-confirm', [
44             'code' => $otp,
45         ]);
46         $resp->assertRedirect('/mfa/setup');
47
48         // Confirmation of setup
49         $resp = $this->followRedirects($resp);
50         $resp->assertSee('Multi-factor method successfully configured');
51         $resp->assertElementContains('a[href$="/mfa/totp-generate"]', 'Reconfigure');
52
53         $this->assertDatabaseHas('mfa_values', [
54             'user_id' => $editor->id,
55             'method' => 'totp',
56         ]);
57         $this->assertFalse(session()->has('mfa-setup-totp-secret'));
58         $value = MfaValue::query()->where('user_id', '=', $editor->id)
59             ->where('method', '=', 'totp')->first();
60         $this->assertEquals($secret, decrypt($value->value));
61     }
62
63     public function test_backup_codes_setup()
64     {
65         $editor = $this->getEditor();
66         $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
67
68         // Setup page state
69         $resp = $this->actingAs($editor)->get('/mfa/setup');
70         $resp->assertElementContains('a[href$="/mfa/backup-codes-generate"]', 'Setup');
71
72         // Generate page access
73         $resp = $this->get('/mfa/backup-codes-generate');
74         $resp->assertSee('Backup Codes');
75         $resp->assertElementContains('form[action$="/mfa/backup-codes-confirm"]', 'Confirm and Enable');
76         $this->assertSessionHas('mfa-setup-backup-codes');
77         $codes = decrypt(session()->get('mfa-setup-backup-codes'));
78         // Check code format
79         $this->assertCount(16, $codes);
80         $this->assertEquals(16*11, strlen(implode('', $codes)));
81         // Check download link
82         $resp->assertSee(base64_encode(implode("\n\n", $codes)));
83
84         // Confirm submit
85         $resp = $this->post('/mfa/backup-codes-confirm');
86         $resp->assertRedirect('/mfa/setup');
87
88         // Confirmation of setup
89         $resp = $this->followRedirects($resp);
90         $resp->assertSee('Multi-factor method successfully configured');
91         $resp->assertElementContains('a[href$="/mfa/backup-codes-generate"]', 'Reconfigure');
92
93         $this->assertDatabaseHas('mfa_values', [
94             'user_id' => $editor->id,
95             'method' => 'backup_codes',
96         ]);
97         $this->assertFalse(session()->has('mfa-setup-backup-codes'));
98         $value = MfaValue::query()->where('user_id', '=', $editor->id)
99             ->where('method', '=', 'backup_codes')->first();
100         $this->assertEquals($codes, json_decode(decrypt($value->value)));
101     }
102
103     public function test_backup_codes_cannot_be_confirmed_if_not_previously_generated()
104     {
105         $resp = $this->asEditor()->post('/mfa/backup-codes-confirm');
106         $resp->assertStatus(500);
107     }
108
109     public function test_mfa_method_count_is_visible_on_user_edit_page()
110     {
111         $admin = $this->getAdmin();
112         $resp = $this->actingAs($admin)->get($admin->getEditUrl());
113         $resp->assertSee('0 methods configured');
114
115         MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');
116         $resp = $this->actingAs($admin)->get($admin->getEditUrl());
117         $resp->assertSee('1 method configured');
118
119         MfaValue::upsertWithValue($admin, MfaValue::METHOD_BACKUP_CODES, 'test');
120         $resp = $this->actingAs($admin)->get($admin->getEditUrl());
121         $resp->assertSee('2 methods configured');
122     }
123
124     public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page()
125     {
126         $admin = $this->getAdmin();
127         $resp = $this->actingAs($admin)->get($admin->getEditUrl());
128         $resp->assertElementExists('a[href$="/mfa/setup"]');
129
130         $resp = $this->actingAs($admin)->get($this->getEditor()->getEditUrl());
131         $resp->assertElementNotExists('a[href$="/mfa/setup"]');
132     }
133
134 }