X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/dabe79a438f22612e7d68c8d1de7817505b59b93..refs/pull/3850/head:/app/Http/Controllers/Auth/LoginController.php diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index b1d22d57e..e16feb079 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,167 +2,232 @@ 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('/https/source.bookstackapp.com/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']); } }