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 Illuminate\Support\Facades\DB;
12 use Illuminate\Support\Facades\Notification;
14 use Tests\TestResponse;
16 class AuthTest extends TestCase
18 public function test_auth_working()
20 $this->get('/')->assertRedirect('/login');
23 public function test_login()
25 $this->login('admin@admin.com', 'password')->assertRedirect('/');
28 public function test_public_viewing()
30 $this->setSettings(['app-public' => 'true']);
33 ->assertSee('Log in');
36 public function test_registration_showing()
38 // Ensure registration form is showing
39 $this->setSettings(['registration-enabled' => 'true']);
41 ->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
44 public function test_normal_registration()
46 // Set settings and get user instance
47 /** @var Role $registrationRole */
48 $registrationRole = Role::query()->first();
49 $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);
50 /** @var User $user */
51 $user = User::factory()->make();
53 // Test form and ensure user is created
54 $this->get('/register')
55 ->assertSee('Sign Up')
56 ->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
58 $resp = $this->post('/register', $user->only('password', 'name', 'email'));
59 $resp->assertRedirect('/');
61 $resp = $this->get('/');
63 $resp->assertSee($user->name);
65 $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
67 $user = User::query()->where('email', '=', $user->email)->first();
68 $this->assertEquals(1, $user->roles()->count());
69 $this->assertEquals($registrationRole->id, $user->roles()->first()->id);
72 public function test_empty_registration_redirects_back_with_errors()
74 // Set settings and get user instance
75 $this->setSettings(['registration-enabled' => 'true']);
77 // Test form and ensure user is created
78 $this->get('/register');
79 $this->post('/register', [])->assertRedirect('/register');
80 $this->get('/register')->assertSee('The name field is required');
83 public function test_registration_validation()
85 $this->setSettings(['registration-enabled' => 'true']);
87 $this->get('/register');
88 $resp = $this->followingRedirects()->post('/register', [
93 $resp->assertSee('The name must be at least 2 characters.');
94 $resp->assertSee('The email must be a valid email address.');
95 $resp->assertSee('The password must be at least 8 characters.');
98 public function test_sign_up_link_on_login()
100 $this->get('/login')->assertDontSee('Sign up');
102 $this->setSettings(['registration-enabled' => 'true']);
104 $this->get('/login')->assertSee('Sign up');
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 = User::factory()->make();
116 // Go through registration process
117 $resp = $this->post('/register', $user->only('name', 'email', 'password'));
118 $resp->assertRedirect('/register/confirm');
119 $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
121 // Ensure notification sent
122 /** @var User $dbUser */
123 $dbUser = User::query()->where('email', '=', $user->email)->first();
124 Notification::assertSentTo($dbUser, ConfirmEmail::class);
126 // Test access and resend confirmation email
127 $resp = $this->login($user->email, $user->password);
128 $resp->assertRedirect('/register/confirm/awaiting');
130 $resp = $this->get('/register/confirm/awaiting');
131 $resp->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
133 $this->get('/books')->assertRedirect('/login');
134 $this->post('/register/confirm/resend', $user->only('email'));
136 // Get confirmation and confirm notification matches
137 $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
138 Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
139 return $notification->token === $emailConfirmation->token;
142 // Check confirmation email confirmation activation.
143 $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login');
144 $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
145 $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
146 $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
149 public function test_restricted_registration()
151 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
152 $user = User::factory()->make();
154 // Go through registration process
155 $this->post('/register', $user->only('name', 'email', 'password'))
156 ->assertRedirect('/register');
157 $resp = $this->get('/register');
158 $resp->assertSee('That email domain does not have access to this application');
159 $this->assertDatabaseMissing('users', $user->only('email'));
161 $user->email = 'barry@example.com';
163 $this->post('/register', $user->only('name', 'email', 'password'))
164 ->assertRedirect('/register/confirm');
165 $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
167 $this->assertNull(auth()->user());
169 $this->get('/')->assertRedirect('/login');
170 $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));
171 $resp->assertSee('Email Address Not Confirmed');
172 $this->assertNull(auth()->user());
175 public function test_restricted_registration_with_confirmation_disabled()
177 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
178 $user = User::factory()->make();
180 // Go through registration process
181 $this->post('/register', $user->only('name', 'email', 'password'))
182 ->assertRedirect('/register');
183 $this->assertDatabaseMissing('users', $user->only('email'));
184 $this->get('/register')->assertSee('That email domain does not have access to this application');
186 $user->email = 'barry@example.com';
188 $this->post('/register', $user->only('name', 'email', 'password'))
189 ->assertRedirect('/register/confirm');
190 $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
192 $this->assertNull(auth()->user());
194 $this->get('/')->assertRedirect('/login');
195 $resp = $this->post('/login', $user->only('email', 'password'));
196 $resp->assertRedirect('/register/confirm/awaiting');
197 $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');
198 $this->assertNull(auth()->user());
201 public function test_registration_role_unset_by_default()
203 $this->assertFalse(setting('registration-role'));
205 $resp = $this->asAdmin()->get('/settings/registration');
206 $resp->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
209 public function test_logout()
211 $this->asAdmin()->get('/')->assertOk();
212 $this->post('/logout')->assertRedirect('/');
213 $this->get('/')->assertRedirect('/login');
216 public function test_mfa_session_cleared_on_logout()
218 $user = $this->getEditor();
219 $mfaSession = $this->app->make(MfaSession::class);
221 $mfaSession->markVerifiedForUser($user);
222 $this->assertTrue($mfaSession->isVerifiedForUser($user));
224 $this->asAdmin()->post('/logout');
225 $this->assertFalse($mfaSession->isVerifiedForUser($user));
228 public function test_reset_password_flow()
230 Notification::fake();
233 ->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
235 $this->get('/password/email')
236 ->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
238 $resp = $this->post('/password/email', [
239 'email' => 'admin@admin.com',
241 $resp->assertRedirect('/password/email');
243 $resp = $this->get('/password/email');
244 $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.');
246 $this->assertDatabaseHas('password_resets', [
247 'email' => 'admin@admin.com',
250 /** @var User $user */
251 $user = User::query()->where('email', '=', 'admin@admin.com')->first();
253 Notification::assertSentTo($user, ResetPassword::class);
254 $n = Notification::sent($user, ResetPassword::class);
256 $this->get('/password/reset/' . $n->first()->token)
258 ->assertSee('Reset Password');
260 $resp = $this->post('/password/reset', [
261 'email' => 'admin@admin.com',
262 'password' => 'randompass',
263 'password_confirmation' => 'randompass',
264 'token' => $n->first()->token,
266 $resp->assertRedirect('/');
268 $this->get('/')->assertSee('Your password has been successfully reset');
271 public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
273 $this->get('/password/email');
274 $resp = $this->followingRedirects()->post('/password/email', [
275 'email' => 'barry@admin.com',
277 $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.');
278 $resp->assertDontSee('We can\'t find a user');
280 $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');
281 $resp = $this->post('/password/reset', [
282 'email' => 'barry@admin.com',
283 'password' => 'randompass',
284 'password_confirmation' => 'randompass',
285 'token' => 'arandometokenvalue',
287 $resp->assertRedirect('/password/reset/arandometokenvalue');
289 $this->get('/password/reset/arandometokenvalue')
290 ->assertDontSee('We can\'t find a user')
291 ->assertSee('The password reset token is invalid for this email address.');
294 public function test_reset_password_page_shows_sign_links()
296 $this->setSettings(['registration-enabled' => 'true']);
297 $this->get('/password/email')
298 ->assertElementContains('a', 'Log in')
299 ->assertElementContains('a', 'Sign up');
302 public function test_reset_password_request_is_throttled()
304 $editor = $this->getEditor();
305 Notification::fake();
306 $this->get('/password/email');
307 $this->followingRedirects()->post('/password/email', [
308 'email' => $editor->email,
311 $resp = $this->followingRedirects()->post('/password/email', [
312 'email' => $editor->email,
314 Notification::assertTimesSent(1, ResetPassword::class);
315 $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
318 public function test_login_redirects_to_initially_requested_url_correctly()
320 config()->set('app.url', 'http://localhost');
321 /** @var Page $page */
322 $page = Page::query()->first();
324 $this->get($page->getUrl())->assertRedirect(url('/login'));
325 $this->login('admin@admin.com', 'password')
326 ->assertRedirect($page->getUrl());
329 public function test_login_intended_redirect_does_not_redirect_to_external_pages()
331 config()->set('app.url', 'http://localhost');
332 $this->setSettings(['app-public' => true]);
334 $this->get('/login', ['referer' => 'https://example.com']);
335 $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
337 $login->assertRedirect('http://localhost');
340 public function test_login_intended_redirect_does_not_factor_mfa_routes()
342 $this->get('/books')->assertRedirect('/login');
343 $this->get('/mfa/setup')->assertRedirect('/login');
344 $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
345 $login->assertRedirect('/books');
348 public function test_login_authenticates_admins_on_all_guards()
350 $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
351 $this->assertTrue(auth()->check());
352 $this->assertTrue(auth('ldap')->check());
353 $this->assertTrue(auth('saml2')->check());
354 $this->assertTrue(auth('oidc')->check());
357 public function test_login_authenticates_nonadmins_on_default_guard_only()
359 $editor = $this->getEditor();
360 $editor->password = bcrypt('password');
363 $this->post('/login', ['email' => $editor->email, 'password' => 'password']);
364 $this->assertTrue(auth()->check());
365 $this->assertFalse(auth('ldap')->check());
366 $this->assertFalse(auth('saml2')->check());
367 $this->assertFalse(auth('oidc')->check());
370 public function test_failed_logins_are_logged_when_message_configured()
372 $log = $this->withTestLogger();
373 config()->set(['logging.failed_login.message' => 'Failed login for %u']);
375 $this->post('/login', ['email' => 'admin@example.com', 'password' => 'cattreedog']);
376 $this->assertTrue($log->hasWarningThatContains('Failed login for admin@example.com'));
378 $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
379 $this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com'));
382 public function test_logged_in_user_with_unconfirmed_email_is_logged_out()
384 $this->setSettings(['registration-confirmation' => 'true']);
385 $user = $this->getEditor();
386 $user->email_confirmed = false;
389 auth()->login($user);
390 $this->assertTrue(auth()->check());
392 $this->get('/books')->assertRedirect('/');
393 $this->assertFalse(auth()->check());
399 protected function login(string $email, string $password): TestResponse
401 return $this->post('/login', compact('email', 'password'));