Auth: Added specific guards against guest account login

Hardened things to enforce the intent that the guest account should not
be used for logins.
Currently this would not be allowed due to empty set password, and no
password fields on user edit forms, but an error could occur if the
login was attempted.

This adds:
- Handling to show normal invalid user warning on login instead of a
  hash check error.
- Prevention of guest user via main login route, in the event that
  inventive workarounds would be used by admins to set a password for
  this account.
- Test for guest user login.
This commit is contained in:
Dan Brown 2024-12-11 14:22:48 +00:00
parent 8ec26e8083
commit 5632fef621
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 59 additions and 3 deletions

View File

@ -5,6 +5,7 @@ namespace BookStack\Access;
use BookStack\Access\Mfa\MfaSession;
use BookStack\Activity\ActivityType;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\LoginAttemptInvalidUserException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
@ -29,10 +30,14 @@ class LoginService
* a reason to (MFA or Unconfirmed Email).
* Returns a boolean to indicate the current login result.
*
* @throws StoppedAuthenticationException
* @throws StoppedAuthenticationException|LoginAttemptInvalidUserException
*/
public function login(User $user, string $method, bool $remember = false): void
{
if ($user->isGuest()) {
throw new LoginAttemptInvalidUserException('Login not allowed for guest user');
}
if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
$this->setLastLoginAttemptedForUser($user, $method, $remember);
@ -58,7 +63,7 @@ class LoginService
*
* @throws Exception
*/
public function reattemptLoginFor(User $user)
public function reattemptLoginFor(User $user): void
{
if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
throw new Exception('Login reattempt user does align with current session state');
@ -152,16 +157,40 @@ class LoginService
*/
public function attempt(array $credentials, string $method, bool $remember = false): bool
{
if ($this->areCredentialsForGuest($credentials)) {
return false;
}
$result = auth()->attempt($credentials, $remember);
if ($result) {
$user = auth()->user();
auth()->logout();
$this->login($user, $method, $remember);
try {
$this->login($user, $method, $remember);
} catch (LoginAttemptInvalidUserException $e) {
// Catch and return false for non-login accounts
// so it looks like a normal invalid login.
return false;
}
}
return $result;
}
/**
* Check if the given credentials are likely for the system guest account.
*/
protected function areCredentialsForGuest(array $credentials): bool
{
if (isset($credentials['email'])) {
return User::query()->where('email', '=', $credentials['email'])
->where('system_name', '=', 'public')
->exists();
}
return false;
}
/**
* Logs the current user out of the application.
* Returns an app post-redirect path.

View File

@ -0,0 +1,7 @@
<?php
namespace BookStack\Exceptions;
class LoginAttemptInvalidUserException extends LoginAttemptException
{
}

View File

@ -3,6 +3,7 @@
namespace Tests\Auth;
use BookStack\Access\Mfa\MfaSession;
use Illuminate\Support\Facades\Hash;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
@ -144,6 +145,25 @@ class AuthTest extends TestCase
$resp->assertSee('Too many login attempts. Please try again in');
}
public function test_login_specifically_disabled_for_guest_account()
{
$guest = $this->users->guest();
$resp = $this->post('/login', ['email' => $guest->email, 'password' => 'password']);
$resp->assertRedirect('/login');
$resp = $this->followRedirects($resp);
$resp->assertSee('These credentials do not match our records.');
// Test login even with password somehow set
$guest->password = Hash::make('password');
$guest->save();
$resp = $this->post('/login', ['email' => $guest->email, 'password' => 'password']);
$resp->assertRedirect('/login');
$resp = $this->followRedirects($resp);
$resp->assertSee('These credentials do not match our records.');
}
/**
* Perform a login.
*/