namespace BookStack\Http\Controllers\Auth;
-use BookStack\Auth\Access\LdapService;
+use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\SocialAuthService;
-use BookStack\Auth\UserRepo;
-use BookStack\Exceptions\AuthException;
+use BookStack\Exceptions\LoginAttemptEmailNeededException;
+use BookStack\Exceptions\LoginAttemptException;
+use BookStack\Facades\Activity;
use BookStack\Http\Controllers\Controller;
-use Illuminate\Contracts\Auth\Authenticatable;
-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;
+ use ThrottlesLogins;
- /**
- * Where to redirect users after login.
- *
- * @var string
- */
- protected $redirectTo = '/';
-
- protected $redirectPath = '/';
- protected $redirectAfterLogout = '/login';
-
- protected $socialAuthService;
- protected $ldapService;
- protected $userRepo;
+ protected SocialAuthService $socialAuthService;
+ protected LoginService $loginService;
/**
* Create a new controller instance.
- *
- * @param \BookStack\Auth\\BookStack\Auth\Access\SocialAuthService $socialAuthService
- * @param LdapService $ldapService
- * @param \BookStack\Auth\UserRepo $userRepo
*/
- public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
+ public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
{
- $this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
- $this->socialAuthService = $socialAuthService;
- $this->ldapService = $ldapService;
- $this->userRepo = $userRepo;
- $this->redirectPath = url('/');
- $this->redirectAfterLogout = url('/login');
- parent::__construct();
- }
+ $this->middleware('guest', ['only' => ['getLogin', 'login']]);
+ $this->middleware('guard:standard,ldap', ['only' => ['login']]);
+ $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
- public function username()
- {
- return config('auth.method') === 'standard' ? 'email' : 'username';
+ $this->socialAuthService = $socialAuthService;
+ $this->loginService = $loginService;
}
/**
- * Overrides the action when a user is authenticated.
- * If the user authenticated but does not exist in the user table we create them.
- * @param Request $request
- * @param Authenticatable $user
- * @return \Illuminate\Http\RedirectResponse
- * @throws AuthException
- * @throws \BookStack\Exceptions\LdapException
+ * Show the application login form.
*/
- protected function authenticated(Request $request, Authenticatable $user)
+ public function getLogin(Request $request)
{
- // Explicitly log them out for now if they do no exist.
- if (!$user->exists) {
- auth()->logout($user);
+ $socialDrivers = $this->socialAuthService->getActiveDrivers();
+ $authMethod = config('auth.method');
+ $preventInitiation = $request->get('prevent_auto_init') === 'true';
+
+ if ($request->has('email')) {
+ session()->flashInput([
+ 'email' => $request->get('email'),
+ 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
+ ]);
}
- if (!$user->exists && $user->email === null && !$request->filled('email')) {
- $request->flash();
- session()->flash('request-email', true);
- return redirect('/login');
+ // Store the previous location for redirect after login
+ $this->updateIntendedFromPrevious();
+
+ if (!$preventInitiation && $this->shouldAutoInitiate()) {
+ return view('auth.login-initiate', [
+ 'authMethod' => $authMethod,
+ ]);
}
- if (!$user->exists && $user->email === null && $request->filled('email')) {
- $user->email = $request->get('email');
+ return view('auth.login', [
+ 'socialDrivers' => $socialDrivers,
+ 'authMethod' => $authMethod,
+ ]);
+ }
+
+ /**
+ * Handle a login request to the application.
+ */
+ public function login(Request $request)
+ {
+ $this->validateLogin($request);
+ $username = $request->get($this->username());
+
+ // Check login throttling attempts to see if they've gone over the limit
+ if ($this->hasTooManyLoginAttempts($request)) {
+ Activity::logFailedLogin($username);
+ return $this->sendLockoutResponse($request);
}
- if (!$user->exists) {
- // Check for users with same email already
- $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
- if ($alreadyUser) {
- throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
+ try {
+ if ($this->attemptLogin($request)) {
+ return $this->sendLoginResponse($request);
}
+ } catch (LoginAttemptException $exception) {
+ Activity::logFailedLogin($username);
- $user->save();
- $this->userRepo->attachDefaultRole($user);
- $this->userRepo->downloadAndAssignUserAvatar($user);
- auth()->login($user);
+ return $this->sendLoginAttemptExceptionResponse($exception, $request);
}
- // Sync LDAP groups if required
- if ($this->ldapService->shouldSyncGroups()) {
- $this->ldapService->syncGroups($user, $request->get($this->username()));
- }
+ // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.
+ $this->incrementLoginAttempts($request);
+ Activity::logFailedLogin($username);
+
+ // 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('/');
}
/**
- * Show the application login form.
- * @param Request $request
- * @return \Illuminate\Http\Response
+ * Attempt to log the user into the application.
*/
- public function getLogin(Request $request)
+ protected function attemptLogin(Request $request): bool
{
- $socialDrivers = $this->socialAuthService->getActiveDrivers();
+ return $this->loginService->attempt(
+ $this->credentials($request),
+ auth()->getDefaultDriver(),
+ $request->filled('remember')
+ );
+ }
+
+
+ /**
+ * Validate the user login request.
+ * @throws ValidationException
+ */
+ protected function validateLogin(Request $request): void
+ {
+ $rules = ['password' => ['required', 'string']];
$authMethod = config('auth.method');
- $samlEnabled = config('saml2.enabled') === true;
- if ($request->has('email')) {
- session()->flashInput([
- 'email' => $request->get('email'),
- 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : ''
- ]);
+ if ($authMethod === 'standard') {
+ $rules['email'] = ['required', 'email'];
}
- return view('auth.login', [
- 'socialDrivers' => $socialDrivers,
- 'authMethod' => $authMethod,
- 'samlEnabled' => $samlEnabled,
- ]);
+ if ($authMethod === 'ldap') {
+ $rules['username'] = ['required', 'string'];
+ $rules['email'] = ['email'];
+ }
+
+ $request->validate($rules);
}
/**
- * Redirect to the relevant social site.
- * @param $socialDriver
- * @return \Symfony\Component\HttpFoundation\RedirectResponse
- * @throws \BookStack\Exceptions\SocialDriverNotConfigured
+ * Send a response when a login attempt exception occurs.
*/
- public function getSocialLogin($socialDriver)
+ protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)
{
- session()->put('social-callback', 'login');
- return $this->socialAuthService->startLogIn($socialDriver);
+ if ($exception instanceof LoginAttemptEmailNeededException) {
+ $request->flash();
+ session()->flash('request-email', true);
+ }
+
+ if ($message = $exception->getMessage()) {
+ $this->showWarningNotification($message);
+ }
+
+ return redirect('/login');
}
/**
- * Log the user out of the application.
- *
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\Response
+ * Update the intended URL location from their previous URL.
+ * Ignores if not from the current app instance or if from certain
+ * login or authentication routes.
*/
- public function logout(Request $request)
+ protected function updateIntendedFromPrevious(): void
{
- if (config('saml2.enabled') && session()->get('last_login_type') === 'saml2') {
- return redirect('/saml2/logout');
+ // Store the previous location for redirect after login
+ $previous = url()->previous('');
+ $isPreviousFromInstance = (strpos($previous, url('/')) === 0);
+ if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
+ return;
}
- $this->guard()->logout();
+ $ignorePrefixList = [
+ '/login',
+ '/mfa',
+ ];
- $request->session()->invalidate();
+ foreach ($ignorePrefixList as $ignorePrefix) {
+ if (strpos($previous, url($ignorePrefix)) === 0) {
+ return;
+ }
+ }
+
+ redirect()->setIntendedUrl($previous);
+ }
+
+ /**
+ * Check if login auto-initiate should be valid based upon authentication config.
+ */
+ protected function shouldAutoInitiate(): bool
+ {
+ $socialDrivers = $this->socialAuthService->getActiveDrivers();
+ $authMethod = config('auth.method');
+ $autoRedirect = config('auth.auto_initiate');
- return $this->loggedOut($request) ?: redirect('/');
+ return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
}
}