diff --git a/app/Auth/Access/LoginService.php b/app/Auth/Access/LoginService.php
index f41570417..c80943166 100644
--- a/app/Auth/Access/LoginService.php
+++ b/app/Auth/Access/LoginService.php
@@ -5,6 +5,7 @@ namespace BookStack\Auth\Access;
 use BookStack\Actions\ActivityType;
 use BookStack\Auth\Access\Mfa\MfaSession;
 use BookStack\Auth\User;
+use BookStack\Exceptions\LoginAttemptException;
 use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Facades\Activity;
 use BookStack\Facades\Theme;
@@ -149,6 +150,7 @@ class LoginService
      * May interrupt the flow if extra authentication requirements are imposed.
      *
      * @throws StoppedAuthenticationException
+     * @throws LoginAttemptException
      */
     public function attempt(array $credentials, string $method, bool $remember = false): bool
     {
diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php
index 28ce96c49..c589fd964 100644
--- a/app/Auth/UserRepo.php
+++ b/app/Auth/UserRepo.php
@@ -10,6 +10,7 @@ use BookStack\Exceptions\UserUpdateException;
 use BookStack\Facades\Activity;
 use BookStack\Uploads\UserAvatars;
 use Exception;
+use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 
@@ -61,7 +62,7 @@ class UserRepo
         $user = new User();
         $user->name = $data['name'];
         $user->email = $data['email'];
-        $user->password = bcrypt(empty($data['password']) ? Str::random(32) : $data['password']);
+        $user->password = Hash::make(empty($data['password']) ? Str::random(32) : $data['password']);
         $user->email_confirmed = $emailConfirmed;
         $user->external_auth_id = $data['external_auth_id'] ?? '';
 
@@ -126,7 +127,7 @@ class UserRepo
         }
 
         if (!empty($data['password'])) {
-            $user->password = bcrypt($data['password']);
+            $user->password = Hash::make($data['password']);
         }
 
         if (!empty($data['language'])) {
diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php
index 873d88475..ea633ff3a 100644
--- a/app/Http/Controllers/Auth/ConfirmEmailController.php
+++ b/app/Http/Controllers/Auth/ConfirmEmailController.php
@@ -14,9 +14,9 @@ use Illuminate\Http\Request;
 
 class ConfirmEmailController extends Controller
 {
-    protected $emailConfirmationService;
-    protected $loginService;
-    protected $userRepo;
+    protected EmailConfirmationService $emailConfirmationService;
+    protected LoginService $loginService;
+    protected UserRepo $userRepo;
 
     /**
      * Create a new controller instance.
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
index b345fad1c..2bdc31df5 100644
--- a/app/Http/Controllers/Auth/ForgotPasswordController.php
+++ b/app/Http/Controllers/Auth/ForgotPasswordController.php
@@ -4,24 +4,11 @@ namespace BookStack\Http\Controllers\Auth;
 
 use BookStack\Actions\ActivityType;
 use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Password;
 
 class ForgotPasswordController extends Controller
 {
-    /*
-    |--------------------------------------------------------------------------
-    | Password Reset Controller
-    |--------------------------------------------------------------------------
-    |
-    | This controller is responsible for handling password reset emails and
-    | includes a trait which assists in sending these notifications from
-    | your application to your users. Feel free to explore this trait.
-    |
-    */
-    use SendsPasswordResetEmails;
-
     /**
      * Create a new controller instance.
      *
@@ -33,6 +20,14 @@ class ForgotPasswordController extends Controller
         $this->middleware('guard:standard');
     }
 
+    /**
+     * Display the form to request a password reset link.
+     */
+    public function showLinkRequestForm()
+    {
+        return view('auth.passwords.email');
+    }
+
     /**
      * Send a reset link to the given user.
      *
@@ -49,7 +44,7 @@ class ForgotPasswordController extends Controller
         // We will send the password reset link to this user. Once we have attempted
         // to send the link, we will examine the response then see the message we
         // need to show to the user. Finally, we'll send out a proper response.
-        $response = $this->broker()->sendResetLink(
+        $response = Password::broker()->sendResetLink(
             $request->only('email')
         );
 
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index 1d6a36c5b..e16feb079 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -8,31 +8,14 @@ use BookStack\Exceptions\LoginAttemptEmailNeededException;
 use BookStack\Exceptions\LoginAttemptException;
 use BookStack\Facades\Activity;
 use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\AuthenticatesUsers;
+use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Validation\ValidationException;
 
 class LoginController extends Controller
 {
-    /*
-    |--------------------------------------------------------------------------
-    | Login Controller
-    |--------------------------------------------------------------------------
-    |
-    | This controller handles authenticating users for the application and
-    | redirecting them to your home screen. The controller uses a trait
-    | to conveniently provide its functionality to your applications.
-    |
-    */
-    use AuthenticatesUsers {
-        logout as traitLogout;
-    }
-
-    /**
-     * Redirection paths.
-     */
-    protected $redirectTo = '/';
-    protected $redirectPath = '/';
+    use ThrottlesLogins;
 
     protected SocialAuthService $socialAuthService;
     protected LoginService $loginService;
@@ -48,21 +31,6 @@ class LoginController extends Controller
 
         $this->socialAuthService = $socialAuthService;
         $this->loginService = $loginService;
-
-        $this->redirectPath = url('/');
-    }
-
-    public function username()
-    {
-        return config('auth.method') === 'standard' ? 'email' : 'username';
-    }
-
-    /**
-     * Get the needed authorization credentials from the request.
-     */
-    protected function credentials(Request $request)
-    {
-        return $request->only('username', 'email', 'password');
     }
 
     /**
@@ -98,29 +66,15 @@ class LoginController extends Controller
 
     /**
      * Handle a login request to the application.
-     *
-     * @param \Illuminate\Http\Request $request
-     *
-     * @throws \Illuminate\Validation\ValidationException
-     *
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
      */
     public function login(Request $request)
     {
         $this->validateLogin($request);
         $username = $request->get($this->username());
 
-        // If the class is using the ThrottlesLogins trait, we can automatically throttle
-        // the login attempts for this application. We'll key this by the username and
-        // the IP address of the client making these requests into this application.
-        if (
-            method_exists($this, 'hasTooManyLoginAttempts') &&
-            $this->hasTooManyLoginAttempts($request)
-        ) {
-            $this->fireLockoutEvent($request);
-
+        // Check login throttling attempts to see if they've gone over the limit
+        if ($this->hasTooManyLoginAttempts($request)) {
             Activity::logFailedLogin($username);
-
             return $this->sendLockoutResponse($request);
         }
 
@@ -134,24 +88,62 @@ class LoginController extends Controller
             return $this->sendLoginAttemptExceptionResponse($exception, $request);
         }
 
-        // If the login attempt was unsuccessful we will increment the number of attempts
-        // to login and redirect the user back to the login form. Of course, when this
-        // user surpasses their maximum number of attempts they will get locked out.
+        // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.
         $this->incrementLoginAttempts($request);
-
         Activity::logFailedLogin($username);
 
-        return $this->sendFailedLoginResponse($request);
+        // Throw validation failure for failed login
+        throw ValidationException::withMessages([
+            $this->username() => [trans('auth.failed')],
+        ])->redirectTo('/login');
+    }
+
+    /**
+     * Logout user and perform subsequent redirect.
+     */
+    public function logout(Request $request)
+    {
+        Auth::guard()->logout();
+        $request->session()->invalidate();
+        $request->session()->regenerateToken();
+
+        $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
+
+        return redirect($redirectUri);
+    }
+
+    /**
+     * Get the expected username input based upon the current auth method.
+     */
+    protected function username(): string
+    {
+        return config('auth.method') === 'standard' ? 'email' : 'username';
+    }
+
+    /**
+     * Get the needed authorization credentials from the request.
+     */
+    protected function credentials(Request $request): array
+    {
+        return $request->only('username', 'email', 'password');
+    }
+
+    /**
+     * Send the response after the user was authenticated.
+     * @return RedirectResponse
+     */
+    protected function sendLoginResponse(Request $request)
+    {
+        $request->session()->regenerate();
+        $this->clearLoginAttempts($request);
+
+        return redirect()->intended('/');
     }
 
     /**
      * Attempt to log the user into the application.
-     *
-     * @param \Illuminate\Http\Request $request
-     *
-     * @return bool
      */
-    protected function attemptLogin(Request $request)
+    protected function attemptLogin(Request $request): bool
     {
         return $this->loginService->attempt(
             $this->credentials($request),
@@ -160,29 +152,12 @@ class LoginController extends Controller
         );
     }
 
-    /**
-     * The user has been authenticated.
-     *
-     * @param \Illuminate\Http\Request $request
-     * @param mixed                    $user
-     *
-     * @return mixed
-     */
-    protected function authenticated(Request $request, $user)
-    {
-        return redirect()->intended($this->redirectPath());
-    }
 
     /**
      * Validate the user login request.
-     *
-     * @param \Illuminate\Http\Request $request
-     *
-     * @throws \Illuminate\Validation\ValidationException
-     *
-     * @return void
+     * @throws ValidationException
      */
-    protected function validateLogin(Request $request)
+    protected function validateLogin(Request $request): void
     {
         $rules = ['password' => ['required', 'string']];
         $authMethod = config('auth.method');
@@ -216,22 +191,6 @@ class LoginController extends Controller
         return redirect('/login');
     }
 
-    /**
-     * Get the failed login response instance.
-     *
-     * @param \Illuminate\Http\Request $request
-     *
-     * @throws \Illuminate\Validation\ValidationException
-     *
-     * @return \Symfony\Component\HttpFoundation\Response
-     */
-    protected function sendFailedLoginResponse(Request $request)
-    {
-        throw ValidationException::withMessages([
-            $this->username() => [trans('auth.failed')],
-        ])->redirectTo('/login');
-    }
-
     /**
      * Update the intended URL location from their previous URL.
      * Ignores if not from the current app instance or if from certain
@@ -271,20 +230,4 @@ class LoginController extends Controller
 
         return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
     }
-
-    /**
-     * Logout user and perform subsequent redirect.
-     *
-     * @param \Illuminate\Http\Request $request
-     *
-     * @return mixed
-     */
-    public function logout(Request $request)
-    {
-        $this->traitLogout($request);
-
-        $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
-
-        return redirect($redirectUri);
-    }
 }
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index 15ee78d50..262ca540e 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -5,42 +5,20 @@ namespace BookStack\Http\Controllers\Auth;
 use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\RegistrationService;
 use BookStack\Auth\Access\SocialAuthService;
-use BookStack\Auth\User;
 use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\RegistersUsers;
+use Illuminate\Contracts\Validation\Validator as ValidatorContract;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Validation\Rules\Password;
 
 class RegisterController extends Controller
 {
-    /*
-    |--------------------------------------------------------------------------
-    | Register Controller
-    |--------------------------------------------------------------------------
-    |
-    | This controller handles the registration of new users as well as their
-    | validation and creation. By default this controller uses a trait to
-    | provide this functionality without requiring any additional code.
-    |
-    */
-    use RegistersUsers;
-
     protected SocialAuthService $socialAuthService;
     protected RegistrationService $registrationService;
     protected LoginService $loginService;
 
-    /**
-     * Where to redirect users after login / registration.
-     *
-     * @var string
-     */
-    protected $redirectTo = '/';
-    protected $redirectPath = '/';
-
     /**
      * Create a new controller instance.
      */
@@ -55,23 +33,6 @@ class RegisterController extends Controller
         $this->socialAuthService = $socialAuthService;
         $this->registrationService = $registrationService;
         $this->loginService = $loginService;
-
-        $this->redirectTo = url('/');
-        $this->redirectPath = url('/');
-    }
-
-    /**
-     * Get a validator for an incoming registration request.
-     *
-     * @return \Illuminate\Contracts\Validation\Validator
-     */
-    protected function validator(array $data)
-    {
-        return Validator::make($data, [
-            'name'     => ['required', 'min:2', 'max:100'],
-            'email'    => ['required', 'email', 'max:255', 'unique:users'],
-            'password' => ['required', Password::default()],
-        ]);
     }
 
     /**
@@ -114,22 +75,18 @@ class RegisterController extends Controller
 
         $this->showSuccessNotification(trans('auth.register_success'));
 
-        return redirect($this->redirectPath());
+        return redirect('/');
     }
 
     /**
-     * Create a new user instance after a valid registration.
-     *
-     * @param array $data
-     *
-     * @return User
+     * Get a validator for an incoming registration request.
      */
-    protected function create(array $data)
+    protected function validator(array $data): ValidatorContract
     {
-        return User::create([
-            'name'     => $data['name'],
-            'email'    => $data['email'],
-            'password' => Hash::make($data['password']),
+        return Validator::make($data, [
+            'name'     => ['required', 'min:2', 'max:100'],
+            'email'    => ['required', 'email', 'max:255', 'unique:users'],
+            'password' => ['required', Password::default()],
         ]);
     }
 }
diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php
index 9df010736..a9914928e 100644
--- a/app/Http/Controllers/Auth/ResetPasswordController.php
+++ b/app/Http/Controllers/Auth/ResetPasswordController.php
@@ -3,65 +3,87 @@
 namespace BookStack\Http\Controllers\Auth;
 
 use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\User;
 use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Password;
+use Illuminate\Support\Str;
+use Illuminate\Validation\Rules\Password as PasswordRule;
 
 class ResetPasswordController extends Controller
 {
-    /*
-    |--------------------------------------------------------------------------
-    | Password Reset Controller
-    |--------------------------------------------------------------------------
-    |
-    | This controller is responsible for handling password reset requests
-    | and uses a simple trait to include this behavior. You're free to
-    | explore this trait and override any methods you wish to tweak.
-    |
-    */
-    use ResetsPasswords;
+    protected LoginService $loginService;
 
-    protected $redirectTo = '/';
-
-    /**
-     * Create a new controller instance.
-     *
-     * @return void
-     */
-    public function __construct()
+    public function __construct(LoginService $loginService)
     {
         $this->middleware('guest');
         $this->middleware('guard:standard');
+
+        $this->loginService = $loginService;
+    }
+
+    /**
+     * Display the password reset view for the given token.
+     * If no token is present, display the link request form.
+     */
+    public function showResetForm(Request $request)
+    {
+        $token = $request->route()->parameter('token');
+
+        return view('auth.passwords.reset')->with(
+            ['token' => $token, 'email' => $request->email]
+        );
+    }
+
+    /**
+     * Reset the given user's password.
+     */
+    public function reset(Request $request)
+    {
+        $request->validate([
+            'token' => 'required',
+            'email' => 'required|email',
+            'password' => ['required', 'confirmed', PasswordRule::defaults()],
+        ]);
+
+        // Here we will attempt to reset the user's password. If it is successful we
+        // will update the password on an actual user model and persist it to the
+        // database. Otherwise we will parse the error and return the response.
+        $credentials = $request->only('email', 'password', 'password_confirmation', 'token');
+        $response = Password::broker()->reset($credentials, function (User $user, string $password) {
+            $user->password = Hash::make($password);
+            $user->setRememberToken(Str::random(60));
+            $user->save();
+
+            $this->loginService->login($user, auth()->getDefaultDriver());
+        });
+
+        // If the password was successfully reset, we will redirect the user back to
+        // the application's home authenticated view. If there is an error we can
+        // redirect them back to where they came from with their error message.
+        return $response === Password::PASSWORD_RESET
+            ? $this->sendResetResponse()
+            : $this->sendResetFailedResponse($request, $response);
     }
 
     /**
      * Get the response for a successful password reset.
-     *
-     * @param Request $request
-     * @param string  $response
-     *
-     * @return \Illuminate\Http\Response
      */
-    protected function sendResetResponse(Request $request, $response)
+    protected function sendResetResponse(): RedirectResponse
     {
-        $message = trans('auth.reset_password_success');
-        $this->showSuccessNotification($message);
+        $this->showSuccessNotification(trans('auth.reset_password_success'));
         $this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());
 
-        return redirect($this->redirectPath())
-            ->with('status', trans($response));
+        return redirect('/');
     }
 
     /**
      * Get the response for a failed password reset.
-     *
-     * @param \Illuminate\Http\Request $request
-     * @param string                   $response
-     *
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
      */
-    protected function sendResetFailedResponse(Request $request, $response)
+    protected function sendResetFailedResponse(Request $request, string $response): RedirectResponse
     {
         // We show invalid users as invalid tokens as to not leak what
         // users may exist in the system.
diff --git a/app/Http/Controllers/Auth/Saml2Controller.php b/app/Http/Controllers/Auth/Saml2Controller.php
index b84483961..b3f8e7601 100644
--- a/app/Http/Controllers/Auth/Saml2Controller.php
+++ b/app/Http/Controllers/Auth/Saml2Controller.php
@@ -9,7 +9,7 @@ use Illuminate\Support\Str;
 
 class Saml2Controller extends Controller
 {
-    protected $samlService;
+    protected Saml2Service $samlService;
 
     /**
      * Saml2Controller constructor.
diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php
index 1691668a2..9ba4028ec 100644
--- a/app/Http/Controllers/Auth/SocialController.php
+++ b/app/Http/Controllers/Auth/SocialController.php
@@ -16,9 +16,9 @@ use Laravel\Socialite\Contracts\User as SocialUser;
 
 class SocialController extends Controller
 {
-    protected $socialAuthService;
-    protected $registrationService;
-    protected $loginService;
+    protected SocialAuthService $socialAuthService;
+    protected RegistrationService $registrationService;
+    protected LoginService $loginService;
 
     /**
      * SocialController constructor.
@@ -28,7 +28,7 @@ class SocialController extends Controller
         RegistrationService $registrationService,
         LoginService $loginService
     ) {
-        $this->middleware('guest')->only(['getRegister', 'postRegister']);
+        $this->middleware('guest')->only(['register']);
         $this->socialAuthService = $socialAuthService;
         $this->registrationService = $registrationService;
         $this->loginService = $loginService;
diff --git a/app/Http/Controllers/Auth/ThrottlesLogins.php b/app/Http/Controllers/Auth/ThrottlesLogins.php
new file mode 100644
index 000000000..7578ba898
--- /dev/null
+++ b/app/Http/Controllers/Auth/ThrottlesLogins.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use Illuminate\Cache\RateLimiter;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Illuminate\Support\Str;
+use Illuminate\Validation\ValidationException;
+
+trait ThrottlesLogins
+{
+    /**
+     * Determine if the user has too many failed login attempts.
+     */
+    protected function hasTooManyLoginAttempts(Request $request): bool
+    {
+        return $this->limiter()->tooManyAttempts(
+            $this->throttleKey($request),
+            $this->maxAttempts()
+        );
+    }
+
+    /**
+     * Increment the login attempts for the user.
+     */
+    protected function incrementLoginAttempts(Request $request): void
+    {
+        $this->limiter()->hit(
+            $this->throttleKey($request),
+            $this->decayMinutes() * 60
+        );
+    }
+
+    /**
+     * Redirect the user after determining they are locked out.
+     * @throws ValidationException
+     */
+    protected function sendLockoutResponse(Request $request): \Symfony\Component\HttpFoundation\Response
+    {
+        $seconds = $this->limiter()->availableIn(
+            $this->throttleKey($request)
+        );
+
+        throw ValidationException::withMessages([
+            $this->username() => [trans('auth.throttle', [
+                'seconds' => $seconds,
+                'minutes' => ceil($seconds / 60),
+            ])],
+        ])->status(Response::HTTP_TOO_MANY_REQUESTS);
+    }
+
+    /**
+     * Clear the login locks for the given user credentials.
+     */
+    protected function clearLoginAttempts(Request $request): void
+    {
+        $this->limiter()->clear($this->throttleKey($request));
+    }
+
+    /**
+     * Get the throttle key for the given request.
+     */
+    protected function throttleKey(Request $request): string
+    {
+        return Str::transliterate(Str::lower($request->input($this->username())) . '|' . $request->ip());
+    }
+
+    /**
+     * Get the rate limiter instance.
+     */
+    protected function limiter(): RateLimiter
+    {
+        return app(RateLimiter::class);
+    }
+
+    /**
+     * Get the maximum number of attempts to allow.
+     */
+    public function maxAttempts(): int
+    {
+        return 5;
+    }
+
+    /**
+     * Get the number of minutes to throttle for.
+     */
+    public function decayMinutes(): int
+    {
+        return 1;
+    }
+}
diff --git a/app/Http/Controllers/Auth/UserInviteController.php b/app/Http/Controllers/Auth/UserInviteController.php
index 27b20f831..5b3bba6ff 100644
--- a/app/Http/Controllers/Auth/UserInviteController.php
+++ b/app/Http/Controllers/Auth/UserInviteController.php
@@ -11,12 +11,13 @@ use Exception;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Routing\Redirector;
+use Illuminate\Support\Facades\Hash;
 use Illuminate\Validation\Rules\Password;
 
 class UserInviteController extends Controller
 {
-    protected $inviteService;
-    protected $userRepo;
+    protected UserInviteService $inviteService;
+    protected UserRepo $userRepo;
 
     /**
      * Create a new controller instance.
@@ -66,7 +67,7 @@ class UserInviteController extends Controller
         }
 
         $user = $this->userRepo->getById($userId);
-        $user->password = bcrypt($request->get('password'));
+        $user->password = Hash::make($request->get('password'));
         $user->email_confirmed = true;
         $user->save();
 
diff --git a/composer.json b/composer.json
index 81896f8f8..59c96ba29 100644
--- a/composer.json
+++ b/composer.json
@@ -26,7 +26,6 @@
         "laravel/framework": "^8.68",
         "laravel/socialite": "^5.2",
         "laravel/tinker": "^2.6",
-        "laravel/ui": "^3.3",
         "league/commonmark": "^1.6",
         "league/flysystem-aws-s3-v3": "^1.0.29",
         "league/html-to-markdown": "^5.0.0",
diff --git a/composer.lock b/composer.lock
index 762fb0a36..1804ef379 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "4a0d254197dda8118685ec1a1eb10edf",
+    "content-hash": "1fed6278d440ef18af1ffa6ca7b29166",
     "packages": [
         {
             "name": "aws/aws-crt-php",
@@ -58,16 +58,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.236.0",
+            "version": "3.236.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "bff1f1ade00c758ea27f498baee1fa16901e5bfd"
+                "reference": "1e8d1abe7582968df16a2e7a87c5dcc51d0dfd1b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bff1f1ade00c758ea27f498baee1fa16901e5bfd",
-                "reference": "bff1f1ade00c758ea27f498baee1fa16901e5bfd",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1e8d1abe7582968df16a2e7a87c5dcc51d0dfd1b",
+                "reference": "1e8d1abe7582968df16a2e7a87c5dcc51d0dfd1b",
                 "shasum": ""
             },
             "require": {
@@ -146,9 +146,9 @@
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.236.0"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.236.1"
             },
-            "time": "2022-09-26T18:13:07+00:00"
+            "time": "2022-09-27T18:19:10+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -2154,67 +2154,6 @@
             },
             "time": "2022-03-23T12:38:24+00:00"
         },
-        {
-            "name": "laravel/ui",
-            "version": "v3.4.6",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/laravel/ui.git",
-                "reference": "65ec5c03f7fee2c8ecae785795b829a15be48c2c"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/laravel/ui/zipball/65ec5c03f7fee2c8ecae785795b829a15be48c2c",
-                "reference": "65ec5c03f7fee2c8ecae785795b829a15be48c2c",
-                "shasum": ""
-            },
-            "require": {
-                "illuminate/console": "^8.42|^9.0",
-                "illuminate/filesystem": "^8.42|^9.0",
-                "illuminate/support": "^8.82|^9.0",
-                "illuminate/validation": "^8.42|^9.0",
-                "php": "^7.3|^8.0"
-            },
-            "require-dev": {
-                "orchestra/testbench": "^6.23|^7.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.x-dev"
-                },
-                "laravel": {
-                    "providers": [
-                        "Laravel\\Ui\\UiServiceProvider"
-                    ]
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Laravel\\Ui\\": "src/",
-                    "Illuminate\\Foundation\\Auth\\": "auth-backend/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Taylor Otwell",
-                    "email": "taylor@laravel.com"
-                }
-            ],
-            "description": "Laravel UI utilities and presets.",
-            "keywords": [
-                "laravel",
-                "ui"
-            ],
-            "support": {
-                "source": "https://github.com/laravel/ui/tree/v3.4.6"
-            },
-            "time": "2022-05-20T13:38:08+00:00"
-        },
         {
             "name": "league/commonmark",
             "version": "1.6.7",
diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php
index 106b71875..849469766 100644
--- a/tests/Auth/AuthTest.php
+++ b/tests/Auth/AuthTest.php
@@ -3,13 +3,7 @@
 namespace Tests\Auth;
 
 use BookStack\Auth\Access\Mfa\MfaSession;
-use BookStack\Auth\Role;
-use BookStack\Auth\User;
 use BookStack\Entities\Models\Page;
-use BookStack\Notifications\ConfirmEmail;
-use BookStack\Notifications\ResetPassword;
-use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Notification;
 use Illuminate\Testing\TestResponse;
 use Tests\TestCase;
 
@@ -33,68 +27,6 @@ class AuthTest extends TestCase
             ->assertSee('Log in');
     }
 
-    public function test_registration_showing()
-    {
-        // Ensure registration form is showing
-        $this->setSettings(['registration-enabled' => 'true']);
-        $resp = $this->get('/login');
-        $this->withHtml($resp)->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
-    }
-
-    public function test_normal_registration()
-    {
-        // Set settings and get user instance
-        /** @var Role $registrationRole */
-        $registrationRole = Role::query()->first();
-        $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);
-        /** @var User $user */
-        $user = User::factory()->make();
-
-        // Test form and ensure user is created
-        $resp = $this->get('/register')
-            ->assertSee('Sign Up');
-        $this->withHtml($resp)->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
-
-        $resp = $this->post('/register', $user->only('password', 'name', 'email'));
-        $resp->assertRedirect('/');
-
-        $resp = $this->get('/');
-        $resp->assertOk();
-        $resp->assertSee($user->name);
-
-        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
-
-        $user = User::query()->where('email', '=', $user->email)->first();
-        $this->assertEquals(1, $user->roles()->count());
-        $this->assertEquals($registrationRole->id, $user->roles()->first()->id);
-    }
-
-    public function test_empty_registration_redirects_back_with_errors()
-    {
-        // Set settings and get user instance
-        $this->setSettings(['registration-enabled' => 'true']);
-
-        // Test form and ensure user is created
-        $this->get('/register');
-        $this->post('/register', [])->assertRedirect('/register');
-        $this->get('/register')->assertSee('The name field is required');
-    }
-
-    public function test_registration_validation()
-    {
-        $this->setSettings(['registration-enabled' => 'true']);
-
-        $this->get('/register');
-        $resp = $this->followingRedirects()->post('/register', [
-            'name'     => '1',
-            'email'    => '1',
-            'password' => '1',
-        ]);
-        $resp->assertSee('The name must be at least 2 characters.');
-        $resp->assertSee('The email must be a valid email address.');
-        $resp->assertSee('The password must be at least 8 characters.');
-    }
-
     public function test_sign_up_link_on_login()
     {
         $this->get('/login')->assertDontSee('Sign up');
@@ -104,108 +36,6 @@ class AuthTest extends TestCase
         $this->get('/login')->assertSee('Sign up');
     }
 
-    public function test_confirmed_registration()
-    {
-        // Fake notifications
-        Notification::fake();
-
-        // Set settings and get user instance
-        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
-        $user = User::factory()->make();
-
-        // Go through registration process
-        $resp = $this->post('/register', $user->only('name', 'email', 'password'));
-        $resp->assertRedirect('/register/confirm');
-        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
-        // Ensure notification sent
-        /** @var User $dbUser */
-        $dbUser = User::query()->where('email', '=', $user->email)->first();
-        Notification::assertSentTo($dbUser, ConfirmEmail::class);
-
-        // Test access and resend confirmation email
-        $resp = $this->login($user->email, $user->password);
-        $resp->assertRedirect('/register/confirm/awaiting');
-
-        $resp = $this->get('/register/confirm/awaiting');
-        $this->withHtml($resp)->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
-
-        $this->get('/books')->assertRedirect('/login');
-        $this->post('/register/confirm/resend', $user->only('email'));
-
-        // Get confirmation and confirm notification matches
-        $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
-        Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
-            return $notification->token === $emailConfirmation->token;
-        });
-
-        // Check confirmation email confirmation activation.
-        $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login');
-        $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
-        $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
-        $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
-    }
-
-    public function test_restricted_registration()
-    {
-        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
-        $user = User::factory()->make();
-
-        // Go through registration process
-        $this->post('/register', $user->only('name', 'email', 'password'))
-            ->assertRedirect('/register');
-        $resp = $this->get('/register');
-        $resp->assertSee('That email domain does not have access to this application');
-        $this->assertDatabaseMissing('users', $user->only('email'));
-
-        $user->email = 'barry@example.com';
-
-        $this->post('/register', $user->only('name', 'email', 'password'))
-            ->assertRedirect('/register/confirm');
-        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
-        $this->assertNull(auth()->user());
-
-        $this->get('/')->assertRedirect('/login');
-        $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));
-        $resp->assertSee('Email Address Not Confirmed');
-        $this->assertNull(auth()->user());
-    }
-
-    public function test_restricted_registration_with_confirmation_disabled()
-    {
-        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
-        $user = User::factory()->make();
-
-        // Go through registration process
-        $this->post('/register', $user->only('name', 'email', 'password'))
-            ->assertRedirect('/register');
-        $this->assertDatabaseMissing('users', $user->only('email'));
-        $this->get('/register')->assertSee('That email domain does not have access to this application');
-
-        $user->email = 'barry@example.com';
-
-        $this->post('/register', $user->only('name', 'email', 'password'))
-            ->assertRedirect('/register/confirm');
-        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
-        $this->assertNull(auth()->user());
-
-        $this->get('/')->assertRedirect('/login');
-        $resp = $this->post('/login', $user->only('email', 'password'));
-        $resp->assertRedirect('/register/confirm/awaiting');
-        $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');
-        $this->assertNull(auth()->user());
-    }
-
-    public function test_registration_role_unset_by_default()
-    {
-        $this->assertFalse(setting('registration-role'));
-
-        $resp = $this->asAdmin()->get('/settings/registration');
-        $this->withHtml($resp)->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
-    }
-
     public function test_logout()
     {
         $this->asAdmin()->get('/')->assertOk();
@@ -225,96 +55,6 @@ class AuthTest extends TestCase
         $this->assertFalse($mfaSession->isVerifiedForUser($user));
     }
 
-    public function test_reset_password_flow()
-    {
-        Notification::fake();
-
-        $resp = $this->get('/login');
-        $this->withHtml($resp)->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
-
-        $resp = $this->get('/password/email');
-        $this->withHtml($resp)->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
-
-        $resp = $this->post('/password/email', [
-            'email' => 'admin@admin.com',
-        ]);
-        $resp->assertRedirect('/password/email');
-
-        $resp = $this->get('/password/email');
-        $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.');
-
-        $this->assertDatabaseHas('password_resets', [
-            'email' => 'admin@admin.com',
-        ]);
-
-        /** @var User $user */
-        $user = User::query()->where('email', '=', 'admin@admin.com')->first();
-
-        Notification::assertSentTo($user, ResetPassword::class);
-        $n = Notification::sent($user, ResetPassword::class);
-
-        $this->get('/password/reset/' . $n->first()->token)
-            ->assertOk()
-            ->assertSee('Reset Password');
-
-        $resp = $this->post('/password/reset', [
-            'email'                 => 'admin@admin.com',
-            'password'              => 'randompass',
-            'password_confirmation' => 'randompass',
-            'token'                 => $n->first()->token,
-        ]);
-        $resp->assertRedirect('/');
-
-        $this->get('/')->assertSee('Your password has been successfully reset');
-    }
-
-    public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
-    {
-        $this->get('/password/email');
-        $resp = $this->followingRedirects()->post('/password/email', [
-            'email' => 'barry@admin.com',
-        ]);
-        $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.');
-        $resp->assertDontSee('We can\'t find a user');
-
-        $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');
-        $resp = $this->post('/password/reset', [
-            'email'                 => 'barry@admin.com',
-            'password'              => 'randompass',
-            'password_confirmation' => 'randompass',
-            'token'                 => 'arandometokenvalue',
-        ]);
-        $resp->assertRedirect('/password/reset/arandometokenvalue');
-
-        $this->get('/password/reset/arandometokenvalue')
-            ->assertDontSee('We can\'t find a user')
-            ->assertSee('The password reset token is invalid for this email address.');
-    }
-
-    public function test_reset_password_page_shows_sign_links()
-    {
-        $this->setSettings(['registration-enabled' => 'true']);
-        $resp = $this->get('/password/email');
-        $this->withHtml($resp)->assertElementContains('a', 'Log in')
-            ->assertElementContains('a', 'Sign up');
-    }
-
-    public function test_reset_password_request_is_throttled()
-    {
-        $editor = $this->getEditor();
-        Notification::fake();
-        $this->get('/password/email');
-        $this->followingRedirects()->post('/password/email', [
-            'email' => $editor->email,
-        ]);
-
-        $resp = $this->followingRedirects()->post('/password/email', [
-            'email' => $editor->email,
-        ]);
-        Notification::assertTimesSent(1, ResetPassword::class);
-        $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
-    }
-
     public function test_login_redirects_to_initially_requested_url_correctly()
     {
         config()->set('app.url', 'http://localhost');
@@ -393,6 +133,19 @@ class AuthTest extends TestCase
         $this->assertFalse(auth()->check());
     }
 
+    public function test_login_attempts_are_rate_limited()
+    {
+        for ($i = 0; $i < 5; $i++) {
+            $resp = $this->login('bennynotexisting@example.com', 'pw123');
+        }
+        $resp = $this->followRedirects($resp);
+        $resp->assertSee('These credentials do not match our records.');
+
+        // Check the fifth attempt provides a lockout response
+        $resp = $this->followRedirects($this->login('bennynotexisting@example.com', 'pw123'));
+        $resp->assertSee('Too many login attempts. Please try again in');
+    }
+
     /**
      * Perform a login.
      */
diff --git a/tests/Auth/RegistrationTest.php b/tests/Auth/RegistrationTest.php
new file mode 100644
index 000000000..45d265b72
--- /dev/null
+++ b/tests/Auth/RegistrationTest.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Tests\Auth;
+
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Notifications\ConfirmEmail;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Notification;
+use Tests\TestCase;
+
+class RegistrationTest extends TestCase
+{
+    public function test_confirmed_registration()
+    {
+        // Fake notifications
+        Notification::fake();
+
+        // Set settings and get user instance
+        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
+        $user = User::factory()->make();
+
+        // Go through registration process
+        $resp = $this->post('/register', $user->only('name', 'email', 'password'));
+        $resp->assertRedirect('/register/confirm');
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+        // Ensure notification sent
+        /** @var User $dbUser */
+        $dbUser = User::query()->where('email', '=', $user->email)->first();
+        Notification::assertSentTo($dbUser, ConfirmEmail::class);
+
+        // Test access and resend confirmation email
+        $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]);
+        $resp->assertRedirect('/register/confirm/awaiting');
+
+        $resp = $this->get('/register/confirm/awaiting');
+        $this->withHtml($resp)->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
+
+        $this->get('/books')->assertRedirect('/login');
+        $this->post('/register/confirm/resend', $user->only('email'));
+
+        // Get confirmation and confirm notification matches
+        $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
+        Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
+            return $notification->token === $emailConfirmation->token;
+        });
+
+        // Check confirmation email confirmation activation.
+        $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login');
+        $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
+        $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
+        $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
+    }
+
+    public function test_restricted_registration()
+    {
+        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
+        $user = User::factory()->make();
+
+        // Go through registration process
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register');
+        $resp = $this->get('/register');
+        $resp->assertSee('That email domain does not have access to this application');
+        $this->assertDatabaseMissing('users', $user->only('email'));
+
+        $user->email = 'barry@example.com';
+
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register/confirm');
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+        $this->assertNull(auth()->user());
+
+        $this->get('/')->assertRedirect('/login');
+        $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));
+        $resp->assertSee('Email Address Not Confirmed');
+        $this->assertNull(auth()->user());
+    }
+
+    public function test_restricted_registration_with_confirmation_disabled()
+    {
+        $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
+        $user = User::factory()->make();
+
+        // Go through registration process
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register');
+        $this->assertDatabaseMissing('users', $user->only('email'));
+        $this->get('/register')->assertSee('That email domain does not have access to this application');
+
+        $user->email = 'barry@example.com';
+
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register/confirm');
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+        $this->assertNull(auth()->user());
+
+        $this->get('/')->assertRedirect('/login');
+        $resp = $this->post('/login', $user->only('email', 'password'));
+        $resp->assertRedirect('/register/confirm/awaiting');
+        $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');
+        $this->assertNull(auth()->user());
+    }
+
+    public function test_registration_role_unset_by_default()
+    {
+        $this->assertFalse(setting('registration-role'));
+
+        $resp = $this->asAdmin()->get('/settings/registration');
+        $this->withHtml($resp)->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --');
+    }
+
+    public function test_registration_showing()
+    {
+        // Ensure registration form is showing
+        $this->setSettings(['registration-enabled' => 'true']);
+        $resp = $this->get('/login');
+        $this->withHtml($resp)->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
+    }
+
+    public function test_normal_registration()
+    {
+        // Set settings and get user instance
+        /** @var Role $registrationRole */
+        $registrationRole = Role::query()->first();
+        $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]);
+        /** @var User $user */
+        $user = User::factory()->make();
+
+        // Test form and ensure user is created
+        $resp = $this->get('/register')
+            ->assertSee('Sign Up');
+        $this->withHtml($resp)->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
+
+        $resp = $this->post('/register', $user->only('password', 'name', 'email'));
+        $resp->assertRedirect('/');
+
+        $resp = $this->get('/');
+        $resp->assertOk();
+        $resp->assertSee($user->name);
+
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
+
+        $user = User::query()->where('email', '=', $user->email)->first();
+        $this->assertEquals(1, $user->roles()->count());
+        $this->assertEquals($registrationRole->id, $user->roles()->first()->id);
+    }
+
+    public function test_empty_registration_redirects_back_with_errors()
+    {
+        // Set settings and get user instance
+        $this->setSettings(['registration-enabled' => 'true']);
+
+        // Test form and ensure user is created
+        $this->get('/register');
+        $this->post('/register', [])->assertRedirect('/register');
+        $this->get('/register')->assertSee('The name field is required');
+    }
+
+    public function test_registration_validation()
+    {
+        $this->setSettings(['registration-enabled' => 'true']);
+
+        $this->get('/register');
+        $resp = $this->followingRedirects()->post('/register', [
+            'name'     => '1',
+            'email'    => '1',
+            'password' => '1',
+        ]);
+        $resp->assertSee('The name must be at least 2 characters.');
+        $resp->assertSee('The email must be a valid email address.');
+        $resp->assertSee('The password must be at least 8 characters.');
+    }
+}
diff --git a/tests/Auth/ResetPasswordTest.php b/tests/Auth/ResetPasswordTest.php
new file mode 100644
index 000000000..7b2d2e72b
--- /dev/null
+++ b/tests/Auth/ResetPasswordTest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Tests\Auth;
+
+use BookStack\Auth\User;
+use BookStack\Notifications\ResetPassword;
+use Illuminate\Support\Facades\Notification;
+use Tests\TestCase;
+
+class ResetPasswordTest extends TestCase
+{
+    public function test_reset_flow()
+    {
+        Notification::fake();
+
+        $resp = $this->get('/login');
+        $this->withHtml($resp)->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
+
+        $resp = $this->get('/password/email');
+        $this->withHtml($resp)->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
+
+        $resp = $this->post('/password/email', [
+            'email' => 'admin@admin.com',
+        ]);
+        $resp->assertRedirect('/password/email');
+
+        $resp = $this->get('/password/email');
+        $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.');
+
+        $this->assertDatabaseHas('password_resets', [
+            'email' => 'admin@admin.com',
+        ]);
+
+        /** @var User $user */
+        $user = User::query()->where('email', '=', 'admin@admin.com')->first();
+
+        Notification::assertSentTo($user, ResetPassword::class);
+        $n = Notification::sent($user, ResetPassword::class);
+
+        $this->get('/password/reset/' . $n->first()->token)
+            ->assertOk()
+            ->assertSee('Reset Password');
+
+        $resp = $this->post('/password/reset', [
+            'email'                 => 'admin@admin.com',
+            'password'              => 'randompass',
+            'password_confirmation' => 'randompass',
+            'token'                 => $n->first()->token,
+        ]);
+        $resp->assertRedirect('/');
+
+        $this->get('/')->assertSee('Your password has been successfully reset');
+    }
+
+    public function test_reset_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
+    {
+        $this->get('/password/email');
+        $resp = $this->followingRedirects()->post('/password/email', [
+            'email' => 'barry@admin.com',
+        ]);
+        $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.');
+        $resp->assertDontSee('We can\'t find a user');
+
+        $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');
+        $resp = $this->post('/password/reset', [
+            'email'                 => 'barry@admin.com',
+            'password'              => 'randompass',
+            'password_confirmation' => 'randompass',
+            'token'                 => 'arandometokenvalue',
+        ]);
+        $resp->assertRedirect('/password/reset/arandometokenvalue');
+
+        $this->get('/password/reset/arandometokenvalue')
+            ->assertDontSee('We can\'t find a user')
+            ->assertSee('The password reset token is invalid for this email address.');
+    }
+
+    public function test_reset_page_shows_sign_links()
+    {
+        $this->setSettings(['registration-enabled' => 'true']);
+        $resp = $this->get('/password/email');
+        $this->withHtml($resp)->assertElementContains('a', 'Log in')
+            ->assertElementContains('a', 'Sign up');
+    }
+
+    public function test_reset_request_is_throttled()
+    {
+        $editor = $this->getEditor();
+        Notification::fake();
+        $this->get('/password/email');
+        $this->followingRedirects()->post('/password/email', [
+            'email' => $editor->email,
+        ]);
+
+        $resp = $this->followingRedirects()->post('/password/email', [
+            'email' => $editor->email,
+        ]);
+        Notification::assertTimesSent(1, ResetPassword::class);
+        $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
+    }
+}