]> BookStack Code Mirror - bookstack/blob - tests/Auth/AuthTest.php
Fix Crowdin name in the language_request issue template
[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 Illuminate\Support\Facades\DB;
12 use Illuminate\Support\Facades\Notification;
13 use Tests\TestCase;
14 use Tests\TestResponse;
15
16 class AuthTest extends TestCase
17 {
18     public function test_auth_working()
19     {
20         $this->get('/')->assertRedirect('/login');
21     }
22
23     public function test_login()
24     {
25         $this->login('admin@admin.com', 'password')->assertRedirect('/');
26     }
27
28     public function test_public_viewing()
29     {
30         $this->setSettings(['app-public' => 'true']);
31         $this->get('/')
32             ->assertOk()
33             ->assertSee('Log in');
34     }
35
36     public function test_registration_showing()
37     {
38         // Ensure registration form is showing
39         $this->setSettings(['registration-enabled' => 'true']);
40         $this->get('/login')
41             ->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
42     }
43
44     public function test_normal_registration()
45     {
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();
52
53         // Test form and ensure user is created
54         $this->get('/register')
55             ->assertSee('Sign Up')
56             ->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
57
58         $resp = $this->post('/register', $user->only('password', 'name', 'email'));
59         $resp->assertRedirect('/');
60
61         $resp = $this->get('/');
62         $resp->assertOk();
63         $resp->assertSee($user->name);
64
65         $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
66
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);
70     }
71
72     public function test_empty_registration_redirects_back_with_errors()
73     {
74         // Set settings and get user instance
75         $this->setSettings(['registration-enabled' => 'true']);
76
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');
81     }
82
83     public function test_registration_validation()
84     {
85         $this->setSettings(['registration-enabled' => 'true']);
86
87         $this->get('/register');
88         $resp = $this->followingRedirects()->post('/register', [
89             'name'     => '1',
90             'email'    => '1',
91             'password' => '1',
92         ]);
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.');
96     }
97
98     public function test_sign_up_link_on_login()
99     {
100         $this->get('/login')->assertDontSee('Sign up');
101
102         $this->setSettings(['registration-enabled' => 'true']);
103
104         $this->get('/login')->assertSee('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 = User::factory()->make();
115
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]);
120
121         // Ensure notification sent
122         /** @var User $dbUser */
123         $dbUser = User::query()->where('email', '=', $user->email)->first();
124         Notification::assertSentTo($dbUser, ConfirmEmail::class);
125
126         // Test access and resend confirmation email
127         $resp = $this->login($user->email, $user->password);
128         $resp->assertRedirect('/register/confirm/awaiting');
129
130         $resp = $this->get('/register/confirm/awaiting');
131         $resp->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
132
133         $this->get('/books')->assertRedirect('/login');
134         $this->post('/register/confirm/resend', $user->only('email'));
135
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;
140         });
141
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]);
147     }
148
149     public function test_restricted_registration()
150     {
151         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
152         $user = User::factory()->make();
153
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'));
160
161         $user->email = 'barry@example.com';
162
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]);
166
167         $this->assertNull(auth()->user());
168
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());
173     }
174
175     public function test_restricted_registration_with_confirmation_disabled()
176     {
177         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
178         $user = User::factory()->make();
179
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');
185
186         $user->email = 'barry@example.com';
187
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]);
191
192         $this->assertNull(auth()->user());
193
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());
199     }
200
201     public function test_registration_role_unset_by_default()
202     {
203         $this->assertFalse(setting('registration-role'));
204
205         $resp = $this->asAdmin()->get('/settings/registration');
206         $resp->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
207     }
208
209     public function test_logout()
210     {
211         $this->asAdmin()->get('/')->assertOk();
212         $this->post('/logout')->assertRedirect('/');
213         $this->get('/')->assertRedirect('/login');
214     }
215
216     public function test_mfa_session_cleared_on_logout()
217     {
218         $user = $this->getEditor();
219         $mfaSession = $this->app->make(MfaSession::class);
220
221         $mfaSession->markVerifiedForUser($user);
222         $this->assertTrue($mfaSession->isVerifiedForUser($user));
223
224         $this->asAdmin()->post('/logout');
225         $this->assertFalse($mfaSession->isVerifiedForUser($user));
226     }
227
228     public function test_reset_password_flow()
229     {
230         Notification::fake();
231
232         $this->get('/login')
233             ->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
234
235         $this->get('/password/email')
236             ->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
237
238         $resp = $this->post('/password/email', [
239             'email' => 'admin@admin.com',
240         ]);
241         $resp->assertRedirect('/password/email');
242
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.');
245
246         $this->assertDatabaseHas('password_resets', [
247             'email' => 'admin@admin.com',
248         ]);
249
250         /** @var User $user */
251         $user = User::query()->where('email', '=', 'admin@admin.com')->first();
252
253         Notification::assertSentTo($user, ResetPassword::class);
254         $n = Notification::sent($user, ResetPassword::class);
255
256         $this->get('/password/reset/' . $n->first()->token)
257             ->assertOk()
258             ->assertSee('Reset Password');
259
260         $resp = $this->post('/password/reset', [
261             'email'                 => 'admin@admin.com',
262             'password'              => 'randompass',
263             'password_confirmation' => 'randompass',
264             'token'                 => $n->first()->token,
265         ]);
266         $resp->assertRedirect('/');
267
268         $this->get('/')->assertSee('Your password has been successfully reset');
269     }
270
271     public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
272     {
273         $this->get('/password/email');
274         $resp = $this->followingRedirects()->post('/password/email', [
275             'email' => 'barry@admin.com',
276         ]);
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');
279
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',
286         ]);
287         $resp->assertRedirect('/password/reset/arandometokenvalue');
288
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.');
292     }
293
294     public function test_reset_password_page_shows_sign_links()
295     {
296         $this->setSettings(['registration-enabled' => 'true']);
297         $this->get('/password/email')
298             ->assertElementContains('a', 'Log in')
299             ->assertElementContains('a', 'Sign up');
300     }
301
302     public function test_reset_password_request_is_throttled()
303     {
304         $editor = $this->getEditor();
305         Notification::fake();
306         $this->get('/password/email');
307         $this->followingRedirects()->post('/password/email', [
308             'email' => $editor->email,
309         ]);
310
311         $resp = $this->followingRedirects()->post('/password/email', [
312             'email' => $editor->email,
313         ]);
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.');
316     }
317
318     public function test_login_redirects_to_initially_requested_url_correctly()
319     {
320         config()->set('app.url', 'http://localhost');
321         /** @var Page $page */
322         $page = Page::query()->first();
323
324         $this->get($page->getUrl())->assertRedirect(url('/login'));
325         $this->login('admin@admin.com', 'password')
326             ->assertRedirect($page->getUrl());
327     }
328
329     public function test_login_intended_redirect_does_not_redirect_to_external_pages()
330     {
331         config()->set('app.url', 'http://localhost');
332         $this->setSettings(['app-public' => true]);
333
334         $this->get('/login', ['referer' => 'https://example.com']);
335         $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
336
337         $login->assertRedirect('http://localhost');
338     }
339
340     public function test_login_intended_redirect_does_not_factor_mfa_routes()
341     {
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');
346     }
347
348     public function test_login_authenticates_admins_on_all_guards()
349     {
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());
355     }
356
357     public function test_login_authenticates_nonadmins_on_default_guard_only()
358     {
359         $editor = $this->getEditor();
360         $editor->password = bcrypt('password');
361         $editor->save();
362
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());
368     }
369
370     public function test_failed_logins_are_logged_when_message_configured()
371     {
372         $log = $this->withTestLogger();
373         config()->set(['logging.failed_login.message' => 'Failed login for %u']);
374
375         $this->post('/login', ['email' => 'admin@example.com', 'password' => 'cattreedog']);
376         $this->assertTrue($log->hasWarningThatContains('Failed login for admin@example.com'));
377
378         $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
379         $this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com'));
380     }
381
382     public function test_logged_in_user_with_unconfirmed_email_is_logged_out()
383     {
384         $this->setSettings(['registration-confirmation' => 'true']);
385         $user = $this->getEditor();
386         $user->email_confirmed = false;
387         $user->save();
388
389         auth()->login($user);
390         $this->assertTrue(auth()->check());
391
392         $this->get('/books')->assertRedirect('/');
393         $this->assertFalse(auth()->check());
394     }
395
396     /**
397      * Perform a login.
398      */
399     protected function login(string $email, string $password): TestResponse
400     {
401         return $this->post('/login', compact('email', 'password'));
402     }
403 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.