5 use BookStack\Auth\Access\Mfa\MfaValue;
6 use PragmaRX\Google2FA\Google2FA;
9 class MfaConfigurationTest extends TestCase
12 public function test_totp_setup()
14 $editor = $this->getEditor();
15 $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
18 $resp = $this->actingAs($editor)->get('/mfa/setup');
19 $resp->assertElementContains('a[href$="/mfa/totp-generate"]', 'Setup');
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');
29 // Validation error, code should remain the same
30 $resp = $this->post('/mfa/totp-confirm', [
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);
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', [
46 $resp->assertRedirect('/mfa/setup');
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');
53 $this->assertDatabaseHas('mfa_values', [
54 'user_id' => $editor->id,
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));
63 public function test_backup_codes_setup()
65 $editor = $this->getEditor();
66 $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
69 $resp = $this->actingAs($editor)->get('/mfa/setup');
70 $resp->assertElementContains('a[href$="/mfa/backup-codes-generate"]', 'Setup');
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'));
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)));
85 $resp = $this->post('/mfa/backup-codes-confirm');
86 $resp->assertRedirect('/mfa/setup');
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');
93 $this->assertDatabaseHas('mfa_values', [
94 'user_id' => $editor->id,
95 'method' => 'backup_codes',
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)));
103 public function test_backup_codes_cannot_be_confirmed_if_not_previously_generated()
105 $resp = $this->asEditor()->post('/mfa/backup-codes-confirm');
106 $resp->assertStatus(500);