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;
14 use Illuminate\Support\Facades\Notification;
15 use Illuminate\Support\Str;
16 use Tests\BrowserKitTest;
18 class AuthTest extends BrowserKitTest
20 public function test_auth_working()
23 ->seePageIs('/login');
26 public function test_login()
32 public function test_public_viewing()
34 $settings = app(SettingService::class);
35 $settings->put('app-public', 'true');
41 public function test_registration_showing()
43 // Ensure registration form is showing
44 $this->setSettings(['registration-enabled' => 'true']);
45 $this->visit('/login')
48 ->seePageIs('/register');
51 public function test_normal_registration()
53 // Set settings and get user instance
54 $this->setSettings(['registration-enabled' => 'true']);
55 $user = factory(User::class)->make();
57 // Test form and ensure user is created
58 $this->visit('/register')
60 ->type($user->name, '#name')
61 ->type($user->email, '#email')
62 ->type($user->password, '#password')
63 ->press('Create Account')
66 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
69 public function test_empty_registration_redirects_back_with_errors()
71 // Set settings and get user instance
72 $this->setSettings(['registration-enabled' => 'true']);
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');
81 public function test_registration_validation()
83 $this->setSettings(['registration-enabled' => 'true']);
85 $this->visit('/register')
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');
96 public function test_sign_up_link_on_login()
98 $this->visit('/login')
101 $this->setSettings(['registration-enabled' => 'true']);
103 $this->visit('/login')
107 public function test_confirmed_registration()
109 // Fake notifications
110 Notification::fake();
112 // Set settings and get user instance
113 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
114 $user = factory(User::class)->make();
116 // Go through registration process
117 $this->visit('/register')
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]);
126 // Ensure notification sent
127 $dbUser = User::where('email', '=', $user->email)->first();
128 Notification::assertSentTo($dbUser, ConfirmEmail::class);
130 // Test access and resend confirmation email
131 $this->login($user->email, $user->password)
132 ->seePageIs('/register/confirm/awaiting')
135 ->seePageIs('/login')
136 ->visit('/register/confirm/awaiting')
137 ->press('Resend Confirmation Email');
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;
145 // Check confirmation email confirmation activation.
146 $this->visit('/register/confirm/' . $emailConfirmation->token)
149 ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token])
150 ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
153 public function test_restricted_registration()
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');
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]);
177 $this->assertNull(auth()->user());
179 $this->visit('/')->seePageIs('/login')
180 ->type($user->email, '#email')
181 ->type($user->password, '#password')
183 ->seePageIs('/register/confirm/awaiting')
184 ->seeText('Email Address Not Confirmed');
187 public function test_restricted_registration_with_confirmation_disabled()
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');
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]);
211 $this->assertNull(auth()->user());
213 $this->visit('/')->seePageIs('/login')
214 ->type($user->email, '#email')
215 ->type($user->password, '#password')
217 ->seePageIs('/register/confirm/awaiting')
218 ->seeText('Email Address Not Confirmed');
221 public function test_user_creation()
223 /** @var User $user */
224 $user = factory(User::class)->make();
225 $adminRole = Role::getRole('admin');
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')
236 ->seePageIs('/settings/users')
237 ->seeInDatabase('users', $user->only(['name', 'email']))
241 $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
244 public function test_user_updating()
246 $user = $this->getNormalUser();
247 $password = $user->password;
249 ->visit('/settings/users')
251 ->seePageIs('/settings/users/' . $user->id)
253 ->type('Barry Scott', '#name')
255 ->seePageIs('/settings/users')
256 ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
257 ->notSeeInDatabase('users', ['name' => $user->name]);
260 $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
263 public function test_user_password_update()
265 $user = $this->getNormalUser();
266 $userProfilePage = '/settings/users/' . $user->id;
268 ->visit($userProfilePage)
269 ->type('newpassword', '#password')
271 ->seePageIs($userProfilePage)
272 ->see('Password confirmation required')
274 ->type('newpassword', '#password')
275 ->type('newpassword', '#password-confirm')
277 ->seePageIs('/settings/users');
279 $userPassword = User::find($user->id)->password;
280 $this->assertTrue(Hash::check('newpassword', $userPassword));
283 public function test_user_deletion()
285 $userDetails = factory(User::class)->make();
286 $user = $this->getEditor($userDetails->toArray());
289 ->visit('/settings/users/' . $user->id)
290 ->click('Delete User')
293 ->seePageIs('/settings/users')
294 ->notSeeInDatabase('users', ['name' => $user->name]);
297 public function test_user_cannot_be_deleted_if_last_admin()
299 $adminRole = Role::getRole('admin');
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) {
309 // Ensure we currently only have 1 admin user
310 $this->assertEquals(1, $adminRole->users()->count());
311 $user = $adminRole->users->first();
313 $this->asAdmin()->visit('/settings/users/' . $user->id)
314 ->click('Delete User')
316 ->seePageIs('/settings/users/' . $user->id)
317 ->see('You cannot delete the only admin');
320 public function test_logout()
327 ->seePageIs('/login');
330 public function test_mfa_session_cleared_on_logout()
332 $user = $this->getEditor();
333 $mfaSession = $this->app->make(MfaSession::class);
335 $mfaSession->markVerifiedForUser($user);
336 $this->assertTrue($mfaSession->isVerifiedForUser($user));
338 $this->asAdmin()->visit('/logout');
339 $this->assertFalse($mfaSession->isVerifiedForUser($user));
342 public function test_reset_password_flow()
344 Notification::fake();
346 $this->visit('/login')->click('Forgot Password?')
347 ->seePageIs('/password/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.');
352 $this->seeInDatabase('password_resets', [
358 Notification::assertSentTo($user, ResetPassword::class);
359 $n = Notification::sent($user, ResetPassword::class);
361 $this->visit('/password/reset/' . $n->first()->token)
362 ->see('Reset Password')
363 ->submitForm('Reset Password', [
365 'password' => 'randompass',
366 'password_confirmation' => 'randompass',
368 ->see('Your password has been successfully reset');
371 public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
373 $this->visit('/login')->click('Forgot Password?')
374 ->seePageIs('/password/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');
380 $this->visit('/password/reset/arandometokenvalue')
381 ->see('Reset Password')
382 ->submitForm('Reset Password', [
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.');
392 public function test_reset_password_page_shows_sign_links()
394 $this->setSettings(['registration-enabled' => 'true']);
395 $this->visit('/password/email')
397 ->seeLink('Sign up');
400 public function test_login_redirects_to_initially_requested_url_correctly()
402 config()->set('app.url', 'http://localhost');
403 $page = Page::query()->first();
405 $this->visit($page->getUrl())
406 ->seePageUrlIs(url('/login'));
408 ->seePageUrlIs($page->getUrl());
411 public function test_login_intended_redirect_does_not_redirect_to_external_pages()
413 config()->set('app.url', 'http://localhost');
414 $this->setSettings(['app-public' => true]);
416 $this->get('/login', ['referer' => 'https://example.com']);
419 $login->assertRedirectedTo('http://localhost');
422 public function test_login_intended_redirect_does_not_factor_mfa_routes()
424 $this->get('/books')->assertRedirectedTo('/login');
425 $this->get('/mfa/setup')->assertRedirectedTo('/login');
427 $login->assertRedirectedTo('/books');
430 public function test_login_authenticates_admins_on_all_guards()
433 $this->assertTrue(auth()->check());
434 $this->assertTrue(auth('ldap')->check());
435 $this->assertTrue(auth('saml2')->check());
438 public function test_login_authenticates_nonadmins_on_default_guard_only()
440 $editor = $this->getEditor();
441 $editor->password = bcrypt('password');
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());
450 public function test_failed_logins_are_logged_when_message_configured()
452 $log = $this->withTestLogger();
453 config()->set(['logging.failed_login.message' => 'Failed login for %u']);
462 public function test_logged_in_user_with_unconfirmed_email_is_logged_out()
464 $this->setSettings(['registration-confirmation' => 'true']);
465 $user = $this->getEditor();
466 $user->email_confirmed = false;
469 auth()->login($user);
470 $this->assertTrue(auth()->check());
472 $this->get('/books');
473 $this->assertRedirectedTo("/");
475 $this->assertFalse(auth()->check());
481 protected function login(string $email, string $password): AuthTest
483 return $this->visit('/login')
484 ->type($email, '#email')
485 ->type($password, '#password')