3 namespace BookStack\Http\Controllers\Auth;
5 use BookStack\Auth\Access\LoginService;
6 use BookStack\Auth\Access\SocialAuthService;
7 use BookStack\Exceptions\LoginAttemptEmailNeededException;
8 use BookStack\Exceptions\LoginAttemptException;
9 use BookStack\Facades\Activity;
10 use BookStack\Http\Controllers\Controller;
11 use Illuminate\Http\RedirectResponse;
12 use Illuminate\Http\Request;
13 use Illuminate\Support\Facades\Auth;
14 use Illuminate\Validation\ValidationException;
16 class LoginController extends Controller
20 protected SocialAuthService $socialAuthService;
21 protected LoginService $loginService;
24 * Create a new controller instance.
26 public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
28 $this->middleware('guest', ['only' => ['getLogin', 'login']]);
29 $this->middleware('guard:standard,ldap', ['only' => ['login']]);
30 $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
32 $this->socialAuthService = $socialAuthService;
33 $this->loginService = $loginService;
37 * Show the application login form.
39 public function getLogin(Request $request)
41 $socialDrivers = $this->socialAuthService->getActiveDrivers();
42 $authMethod = config('auth.method');
43 $preventInitiation = $request->get('prevent_auto_init') === 'true';
45 if ($request->has('email')) {
46 session()->flashInput([
47 'email' => $request->get('email'),
48 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
52 // Store the previous location for redirect after login
53 $this->updateIntendedFromPrevious();
55 if (!$preventInitiation && $this->shouldAutoInitiate()) {
56 return view('auth.login-initiate', [
57 'authMethod' => $authMethod,
61 return view('auth.login', [
62 'socialDrivers' => $socialDrivers,
63 'authMethod' => $authMethod,
68 * Handle a login request to the application.
70 public function login(Request $request)
72 $this->validateLogin($request);
73 $username = $request->get($this->username());
75 // Check login throttling attempts to see if they've gone over the limit
76 if ($this->hasTooManyLoginAttempts($request)) {
77 Activity::logFailedLogin($username);
78 return $this->sendLockoutResponse($request);
82 if ($this->attemptLogin($request)) {
83 return $this->sendLoginResponse($request);
85 } catch (LoginAttemptException $exception) {
86 Activity::logFailedLogin($username);
88 return $this->sendLoginAttemptExceptionResponse($exception, $request);
91 // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.
92 $this->incrementLoginAttempts($request);
93 Activity::logFailedLogin($username);
95 // Throw validation failure for failed login
96 throw ValidationException::withMessages([
97 $this->username() => [trans('auth.failed')],
98 ])->redirectTo('/login');
102 * Logout user and perform subsequent redirect.
104 public function logout(Request $request)
106 Auth::guard()->logout();
107 $request->session()->invalidate();
108 $request->session()->regenerateToken();
110 $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
112 return redirect($redirectUri);
116 * Get the expected username input based upon the current auth method.
118 protected function username(): string
120 return config('auth.method') === 'standard' ? 'email' : 'username';
124 * Get the needed authorization credentials from the request.
126 protected function credentials(Request $request): array
128 return $request->only('username', 'email', 'password');
132 * Send the response after the user was authenticated.
133 * @return RedirectResponse
135 protected function sendLoginResponse(Request $request)
137 $request->session()->regenerate();
138 $this->clearLoginAttempts($request);
140 return redirect()->intended('/');
144 * Attempt to log the user into the application.
146 protected function attemptLogin(Request $request): bool
148 return $this->loginService->attempt(
149 $this->credentials($request),
150 auth()->getDefaultDriver(),
151 $request->filled('remember')
157 * Validate the user login request.
158 * @throws ValidationException
160 protected function validateLogin(Request $request): void
162 $rules = ['password' => ['required', 'string']];
163 $authMethod = config('auth.method');
165 if ($authMethod === 'standard') {
166 $rules['email'] = ['required', 'email'];
169 if ($authMethod === 'ldap') {
170 $rules['username'] = ['required', 'string'];
171 $rules['email'] = ['email'];
174 $request->validate($rules);
178 * Send a response when a login attempt exception occurs.
180 protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)
182 if ($exception instanceof LoginAttemptEmailNeededException) {
184 session()->flash('request-email', true);
187 if ($message = $exception->getMessage()) {
188 $this->showWarningNotification($message);
191 return redirect('/login');
195 * Update the intended URL location from their previous URL.
196 * Ignores if not from the current app instance or if from certain
197 * login or authentication routes.
199 protected function updateIntendedFromPrevious(): void
201 // Store the previous location for redirect after login
202 $previous = url()->previous('');
203 $isPreviousFromInstance = (strpos($previous, url('/')) === 0);
204 if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
208 $ignorePrefixList = [
213 foreach ($ignorePrefixList as $ignorePrefix) {
214 if (strpos($previous, url($ignorePrefix)) === 0) {
219 redirect()->setIntendedUrl($previous);
223 * Check if login auto-initiate should be valid based upon authentication config.
225 protected function shouldAutoInitiate(): bool
227 $socialDrivers = $this->socialAuthService->getActiveDrivers();
228 $authMethod = config('auth.method');
229 $autoRedirect = config('auth.auto_initiate');
231 return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);