]> BookStack Code Mirror - bookstack/blob - tests/Auth/AuthTest.php
Added back email confirmation check in middleware
[bookstack] / tests / Auth / AuthTest.php
1 <?php
2
3 namespace Tests\Auth;
4
5 use BookStack\Auth\Access\Mfa\MfaSession;
6 use BookStack\Auth\Role;
7 use BookStack\Auth\User;
8 use BookStack\Entities\Models\Page;
9 use BookStack\Notifications\ConfirmEmail;
10 use BookStack\Notifications\ResetPassword;
11 use BookStack\Settings\SettingService;
12 use DB;
13 use Hash;
14 use Illuminate\Support\Facades\Notification;
15 use Illuminate\Support\Str;
16 use Tests\BrowserKitTest;
17
18 class AuthTest extends BrowserKitTest
19 {
20     public function test_auth_working()
21     {
22         $this->visit('/')
23             ->seePageIs('/login');
24     }
25
26     public function test_login()
27     {
28         $this->login('[email protected]', 'password')
29             ->seePageIs('/');
30     }
31
32     public function test_public_viewing()
33     {
34         $settings = app(SettingService::class);
35         $settings->put('app-public', 'true');
36         $this->visit('/')
37             ->seePageIs('/')
38             ->see('Log In');
39     }
40
41     public function test_registration_showing()
42     {
43         // Ensure registration form is showing
44         $this->setSettings(['registration-enabled' => 'true']);
45         $this->visit('/login')
46             ->see('Sign up')
47             ->click('Sign up')
48             ->seePageIs('/register');
49     }
50
51     public function test_normal_registration()
52     {
53         // Set settings and get user instance
54         $this->setSettings(['registration-enabled' => 'true']);
55         $user = factory(User::class)->make();
56
57         // Test form and ensure user is created
58         $this->visit('/register')
59             ->see('Sign Up')
60             ->type($user->name, '#name')
61             ->type($user->email, '#email')
62             ->type($user->password, '#password')
63             ->press('Create Account')
64             ->seePageIs('/')
65             ->see($user->name)
66             ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
67     }
68
69     public function test_empty_registration_redirects_back_with_errors()
70     {
71         // Set settings and get user instance
72         $this->setSettings(['registration-enabled' => 'true']);
73
74         // Test form and ensure user is created
75         $this->visit('/register')
76             ->press('Create Account')
77             ->see('The name field is required')
78             ->seePageIs('/register');
79     }
80
81     public function test_registration_validation()
82     {
83         $this->setSettings(['registration-enabled' => 'true']);
84
85         $this->visit('/register')
86             ->type('1', '#name')
87             ->type('1', '#email')
88             ->type('1', '#password')
89             ->press('Create Account')
90             ->see('The name must be at least 2 characters.')
91             ->see('The email must be a valid email address.')
92             ->see('The password must be at least 8 characters.')
93             ->seePageIs('/register');
94     }
95
96     public function test_sign_up_link_on_login()
97     {
98         $this->visit('/login')
99             ->dontSee('Sign up');
100
101         $this->setSettings(['registration-enabled' => 'true']);
102
103         $this->visit('/login')
104             ->see('Sign up');
105     }
106
107     public function test_confirmed_registration()
108     {
109         // Fake notifications
110         Notification::fake();
111
112         // Set settings and get user instance
113         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
114         $user = factory(User::class)->make();
115
116         // Go through registration process
117         $this->visit('/register')
118             ->see('Sign Up')
119             ->type($user->name, '#name')
120             ->type($user->email, '#email')
121             ->type($user->password, '#password')
122             ->press('Create Account')
123             ->seePageIs('/register/confirm')
124             ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
125
126         // Ensure notification sent
127         $dbUser = User::where('email', '=', $user->email)->first();
128         Notification::assertSentTo($dbUser, ConfirmEmail::class);
129
130         // Test access and resend confirmation email
131         $this->login($user->email, $user->password)
132             ->seePageIs('/register/confirm/awaiting')
133             ->see('Resend')
134             ->visit('/books')
135             ->seePageIs('/login')
136             ->visit('/register/confirm/awaiting')
137             ->press('Resend Confirmation Email');
138
139         // Get confirmation and confirm notification matches
140         $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
141         Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
142             return $notification->token === $emailConfirmation->token;
143         });
144
145         // Check confirmation email confirmation activation.
146         $this->visit('/register/confirm/' . $emailConfirmation->token)
147             ->seePageIs('/')
148             ->see($user->name)
149             ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token])
150             ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
151     }
152
153     public function test_restricted_registration()
154     {
155         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
156         $user = factory(User::class)->make();
157         // Go through registration process
158         $this->visit('/register')
159             ->type($user->name, '#name')
160             ->type($user->email, '#email')
161             ->type($user->password, '#password')
162             ->press('Create Account')
163             ->seePageIs('/register')
164             ->dontSeeInDatabase('users', ['email' => $user->email])
165             ->see('That email domain does not have access to this application');
166
167         $user->email = '[email protected]';
168
169         $this->visit('/register')
170             ->type($user->name, '#name')
171             ->type($user->email, '#email')
172             ->type($user->password, '#password')
173             ->press('Create Account')
174             ->seePageIs('/register/confirm')
175             ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
176
177         $this->assertNull(auth()->user());
178
179         $this->visit('/')->seePageIs('/login')
180             ->type($user->email, '#email')
181             ->type($user->password, '#password')
182             ->press('Log In')
183             ->seePageIs('/register/confirm/awaiting')
184             ->seeText('Email Address Not Confirmed');
185     }
186
187     public function test_restricted_registration_with_confirmation_disabled()
188     {
189         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
190         $user = factory(User::class)->make();
191         // Go through registration process
192         $this->visit('/register')
193             ->type($user->name, '#name')
194             ->type($user->email, '#email')
195             ->type($user->password, '#password')
196             ->press('Create Account')
197             ->seePageIs('/register')
198             ->dontSeeInDatabase('users', ['email' => $user->email])
199             ->see('That email domain does not have access to this application');
200
201         $user->email = '[email protected]';
202
203         $this->visit('/register')
204             ->type($user->name, '#name')
205             ->type($user->email, '#email')
206             ->type($user->password, '#password')
207             ->press('Create Account')
208             ->seePageIs('/register/confirm')
209             ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
210
211         $this->assertNull(auth()->user());
212
213         $this->visit('/')->seePageIs('/login')
214             ->type($user->email, '#email')
215             ->type($user->password, '#password')
216             ->press('Log In')
217             ->seePageIs('/register/confirm/awaiting')
218             ->seeText('Email Address Not Confirmed');
219     }
220
221     public function test_user_creation()
222     {
223         /** @var User $user */
224         $user = factory(User::class)->make();
225         $adminRole = Role::getRole('admin');
226
227         $this->asAdmin()
228             ->visit('/settings/users')
229             ->click('Add New User')
230             ->type($user->name, '#name')
231             ->type($user->email, '#email')
232             ->check("roles[{$adminRole->id}]")
233             ->type($user->password, '#password')
234             ->type($user->password, '#password-confirm')
235             ->press('Save')
236             ->seePageIs('/settings/users')
237             ->seeInDatabase('users', $user->only(['name', 'email']))
238             ->see($user->name);
239
240         $user->refresh();
241         $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
242     }
243
244     public function test_user_updating()
245     {
246         $user = $this->getNormalUser();
247         $password = $user->password;
248         $this->asAdmin()
249             ->visit('/settings/users')
250             ->click($user->name)
251             ->seePageIs('/settings/users/' . $user->id)
252             ->see($user->email)
253             ->type('Barry Scott', '#name')
254             ->press('Save')
255             ->seePageIs('/settings/users')
256             ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
257             ->notSeeInDatabase('users', ['name' => $user->name]);
258
259         $user->refresh();
260         $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
261     }
262
263     public function test_user_password_update()
264     {
265         $user = $this->getNormalUser();
266         $userProfilePage = '/settings/users/' . $user->id;
267         $this->asAdmin()
268             ->visit($userProfilePage)
269             ->type('newpassword', '#password')
270             ->press('Save')
271             ->seePageIs($userProfilePage)
272             ->see('Password confirmation required')
273
274             ->type('newpassword', '#password')
275             ->type('newpassword', '#password-confirm')
276             ->press('Save')
277             ->seePageIs('/settings/users');
278
279         $userPassword = User::find($user->id)->password;
280         $this->assertTrue(Hash::check('newpassword', $userPassword));
281     }
282
283     public function test_user_deletion()
284     {
285         $userDetails = factory(User::class)->make();
286         $user = $this->getEditor($userDetails->toArray());
287
288         $this->asAdmin()
289             ->visit('/settings/users/' . $user->id)
290             ->click('Delete User')
291             ->see($user->name)
292             ->press('Confirm')
293             ->seePageIs('/settings/users')
294             ->notSeeInDatabase('users', ['name' => $user->name]);
295     }
296
297     public function test_user_cannot_be_deleted_if_last_admin()
298     {
299         $adminRole = Role::getRole('admin');
300
301         // Delete all but one admin user if there are more than one
302         $adminUsers = $adminRole->users;
303         if (count($adminUsers) > 1) {
304             foreach ($adminUsers->splice(1) as $user) {
305                 $user->delete();
306             }
307         }
308
309         // Ensure we currently only have 1 admin user
310         $this->assertEquals(1, $adminRole->users()->count());
311         $user = $adminRole->users->first();
312
313         $this->asAdmin()->visit('/settings/users/' . $user->id)
314             ->click('Delete User')
315             ->press('Confirm')
316             ->seePageIs('/settings/users/' . $user->id)
317             ->see('You cannot delete the only admin');
318     }
319
320     public function test_logout()
321     {
322         $this->asAdmin()
323             ->visit('/')
324             ->seePageIs('/')
325             ->visit('/logout')
326             ->visit('/')
327             ->seePageIs('/login');
328     }
329
330     public function test_mfa_session_cleared_on_logout()
331     {
332         $user = $this->getEditor();
333         $mfaSession = $this->app->make(MfaSession::class);
334
335         $mfaSession->markVerifiedForUser($user);
336         $this->assertTrue($mfaSession->isVerifiedForUser($user));
337
338         $this->asAdmin()->visit('/logout');
339         $this->assertFalse($mfaSession->isVerifiedForUser($user));
340     }
341
342     public function test_reset_password_flow()
343     {
344         Notification::fake();
345
346         $this->visit('/login')->click('Forgot Password?')
347             ->seePageIs('/password/email')
348             ->type('[email protected]', 'email')
349             ->press('Send Reset Link')
350             ->see('A password reset link will be sent to [email protected] if that email address is found in the system.');
351
352         $this->seeInDatabase('password_resets', [
353             'email' => '[email protected]',
354         ]);
355
356         $user = User::where('email', '=', '[email protected]')->first();
357
358         Notification::assertSentTo($user, ResetPassword::class);
359         $n = Notification::sent($user, ResetPassword::class);
360
361         $this->visit('/password/reset/' . $n->first()->token)
362             ->see('Reset Password')
363             ->submitForm('Reset Password', [
364                 'email'                 => '[email protected]',
365                 'password'              => 'randompass',
366                 'password_confirmation' => 'randompass',
367             ])->seePageIs('/')
368             ->see('Your password has been successfully reset');
369     }
370
371     public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
372     {
373         $this->visit('/login')->click('Forgot Password?')
374             ->seePageIs('/password/email')
375             ->type('[email protected]', 'email')
376             ->press('Send Reset Link')
377             ->see('A password reset link will be sent to [email protected] if that email address is found in the system.')
378             ->dontSee('We can\'t find a user');
379
380         $this->visit('/password/reset/arandometokenvalue')
381             ->see('Reset Password')
382             ->submitForm('Reset Password', [
383                 'email'                 => '[email protected]',
384                 'password'              => 'randompass',
385                 'password_confirmation' => 'randompass',
386             ])->followRedirects()
387             ->seePageIs('/password/reset/arandometokenvalue')
388             ->dontSee('We can\'t find a user')
389             ->see('The password reset token is invalid for this email address.');
390     }
391
392     public function test_reset_password_page_shows_sign_links()
393     {
394         $this->setSettings(['registration-enabled' => 'true']);
395         $this->visit('/password/email')
396             ->seeLink('Log in')
397             ->seeLink('Sign up');
398     }
399
400     public function test_login_redirects_to_initially_requested_url_correctly()
401     {
402         config()->set('app.url', 'http://localhost');
403         $page = Page::query()->first();
404
405         $this->visit($page->getUrl())
406             ->seePageUrlIs(url('/login'));
407         $this->login('[email protected]', 'password')
408             ->seePageUrlIs($page->getUrl());
409     }
410
411     public function test_login_intended_redirect_does_not_redirect_to_external_pages()
412     {
413         config()->set('app.url', 'http://localhost');
414         $this->setSettings(['app-public' => true]);
415
416         $this->get('/login', ['referer' => 'https://example.com']);
417         $login = $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
418
419         $login->assertRedirectedTo('http://localhost');
420     }
421
422     public function test_login_intended_redirect_does_not_factor_mfa_routes()
423     {
424         $this->get('/books')->assertRedirectedTo('/login');
425         $this->get('/mfa/setup')->assertRedirectedTo('/login');
426         $login = $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
427         $login->assertRedirectedTo('/books');
428     }
429
430     public function test_login_authenticates_admins_on_all_guards()
431     {
432         $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
433         $this->assertTrue(auth()->check());
434         $this->assertTrue(auth('ldap')->check());
435         $this->assertTrue(auth('saml2')->check());
436     }
437
438     public function test_login_authenticates_nonadmins_on_default_guard_only()
439     {
440         $editor = $this->getEditor();
441         $editor->password = bcrypt('password');
442         $editor->save();
443
444         $this->post('/login', ['email' => $editor->email, 'password' => 'password']);
445         $this->assertTrue(auth()->check());
446         $this->assertFalse(auth('ldap')->check());
447         $this->assertFalse(auth('saml2')->check());
448     }
449
450     public function test_failed_logins_are_logged_when_message_configured()
451     {
452         $log = $this->withTestLogger();
453         config()->set(['logging.failed_login.message' => 'Failed login for %u']);
454
455         $this->post('/login', ['email' => '[email protected]', 'password' => 'cattreedog']);
456         $this->assertTrue($log->hasWarningThatContains('Failed login for [email protected]'));
457
458         $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
459         $this->assertFalse($log->hasWarningThatContains('Failed login for [email protected]'));
460     }
461
462     public function test_logged_in_user_with_unconfirmed_email_is_logged_out()
463     {
464         $this->setSettings(['registration-confirmation' => 'true']);
465         $user = $this->getEditor();
466         $user->email_confirmed = false;
467         $user->save();
468
469         auth()->login($user);
470         $this->assertTrue(auth()->check());
471
472         $this->get('/books');
473         $this->assertRedirectedTo("/");
474
475         $this->assertFalse(auth()->check());
476     }
477
478     /**
479      * Perform a login.
480      */
481     protected function login(string $email, string $password): AuthTest
482     {
483         return $this->visit('/login')
484             ->type($email, '#email')
485             ->type($password, '#password')
486             ->press('Log In');
487     }
488 }