]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/Auth/LoginController.php
e16feb0790303efb25a044108c2c5203a3c65683
[bookstack] / app / Http / Controllers / Auth / LoginController.php
1 <?php
2
3 namespace BookStack\Http\Controllers\Auth;
4
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;
15
16 class LoginController extends Controller
17 {
18     use ThrottlesLogins;
19
20     protected SocialAuthService $socialAuthService;
21     protected LoginService $loginService;
22
23     /**
24      * Create a new controller instance.
25      */
26     public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
27     {
28         $this->middleware('guest', ['only' => ['getLogin', 'login']]);
29         $this->middleware('guard:standard,ldap', ['only' => ['login']]);
30         $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
31
32         $this->socialAuthService = $socialAuthService;
33         $this->loginService = $loginService;
34     }
35
36     /**
37      * Show the application login form.
38      */
39     public function getLogin(Request $request)
40     {
41         $socialDrivers = $this->socialAuthService->getActiveDrivers();
42         $authMethod = config('auth.method');
43         $preventInitiation = $request->get('prevent_auto_init') === 'true';
44
45         if ($request->has('email')) {
46             session()->flashInput([
47                 'email'    => $request->get('email'),
48                 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
49             ]);
50         }
51
52         // Store the previous location for redirect after login
53         $this->updateIntendedFromPrevious();
54
55         if (!$preventInitiation && $this->shouldAutoInitiate()) {
56             return view('auth.login-initiate', [
57                 'authMethod'    => $authMethod,
58             ]);
59         }
60
61         return view('auth.login', [
62             'socialDrivers' => $socialDrivers,
63             'authMethod'    => $authMethod,
64         ]);
65     }
66
67     /**
68      * Handle a login request to the application.
69      */
70     public function login(Request $request)
71     {
72         $this->validateLogin($request);
73         $username = $request->get($this->username());
74
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);
79         }
80
81         try {
82             if ($this->attemptLogin($request)) {
83                 return $this->sendLoginResponse($request);
84             }
85         } catch (LoginAttemptException $exception) {
86             Activity::logFailedLogin($username);
87
88             return $this->sendLoginAttemptExceptionResponse($exception, $request);
89         }
90
91         // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.
92         $this->incrementLoginAttempts($request);
93         Activity::logFailedLogin($username);
94
95         // Throw validation failure for failed login
96         throw ValidationException::withMessages([
97             $this->username() => [trans('auth.failed')],
98         ])->redirectTo('/login');
99     }
100
101     /**
102      * Logout user and perform subsequent redirect.
103      */
104     public function logout(Request $request)
105     {
106         Auth::guard()->logout();
107         $request->session()->invalidate();
108         $request->session()->regenerateToken();
109
110         $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
111
112         return redirect($redirectUri);
113     }
114
115     /**
116      * Get the expected username input based upon the current auth method.
117      */
118     protected function username(): string
119     {
120         return config('auth.method') === 'standard' ? 'email' : 'username';
121     }
122
123     /**
124      * Get the needed authorization credentials from the request.
125      */
126     protected function credentials(Request $request): array
127     {
128         return $request->only('username', 'email', 'password');
129     }
130
131     /**
132      * Send the response after the user was authenticated.
133      * @return RedirectResponse
134      */
135     protected function sendLoginResponse(Request $request)
136     {
137         $request->session()->regenerate();
138         $this->clearLoginAttempts($request);
139
140         return redirect()->intended('/');
141     }
142
143     /**
144      * Attempt to log the user into the application.
145      */
146     protected function attemptLogin(Request $request): bool
147     {
148         return $this->loginService->attempt(
149             $this->credentials($request),
150             auth()->getDefaultDriver(),
151             $request->filled('remember')
152         );
153     }
154
155
156     /**
157      * Validate the user login request.
158      * @throws ValidationException
159      */
160     protected function validateLogin(Request $request): void
161     {
162         $rules = ['password' => ['required', 'string']];
163         $authMethod = config('auth.method');
164
165         if ($authMethod === 'standard') {
166             $rules['email'] = ['required', 'email'];
167         }
168
169         if ($authMethod === 'ldap') {
170             $rules['username'] = ['required', 'string'];
171             $rules['email'] = ['email'];
172         }
173
174         $request->validate($rules);
175     }
176
177     /**
178      * Send a response when a login attempt exception occurs.
179      */
180     protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)
181     {
182         if ($exception instanceof LoginAttemptEmailNeededException) {
183             $request->flash();
184             session()->flash('request-email', true);
185         }
186
187         if ($message = $exception->getMessage()) {
188             $this->showWarningNotification($message);
189         }
190
191         return redirect('/login');
192     }
193
194     /**
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.
198      */
199     protected function updateIntendedFromPrevious(): void
200     {
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) {
205             return;
206         }
207
208         $ignorePrefixList = [
209             '/login',
210             '/mfa',
211         ];
212
213         foreach ($ignorePrefixList as $ignorePrefix) {
214             if (strpos($previous, url($ignorePrefix)) === 0) {
215                 return;
216             }
217         }
218
219         redirect()->setIntendedUrl($previous);
220     }
221
222     /**
223      * Check if login auto-initiate should be valid based upon authentication config.
224      */
225     protected function shouldAutoInitiate(): bool
226     {
227         $socialDrivers = $this->socialAuthService->getActiveDrivers();
228         $authMethod = config('auth.method');
229         $autoRedirect = config('auth.auto_initiate');
230
231         return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
232     }
233 }