]> BookStack Code Mirror - bookstack/blob - app/Access/Controllers/LoginController.php
9047366566d8082de71249e602c7cdaebbeecd91
[bookstack] / app / Access / Controllers / LoginController.php
1 <?php
2
3 namespace BookStack\Access\Controllers;
4
5 use BookStack\Access\LoginService;
6 use BookStack\Access\SocialDriverManager;
7 use BookStack\Exceptions\LoginAttemptEmailNeededException;
8 use BookStack\Exceptions\LoginAttemptException;
9 use BookStack\Facades\Activity;
10 use BookStack\Http\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 SocialDriverManager $socialDriverManager;
21     protected LoginService $loginService;
22
23     /**
24      * Create a new controller instance.
25      */
26     public function __construct(SocialDriverManager $driverManager, 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->socialDriverManager = $driverManager;
33         $this->loginService = $loginService;
34     }
35
36     /**
37      * Show the application login form.
38      */
39     public function getLogin(Request $request)
40     {
41         $socialDrivers = $this->socialDriverManager->getActive();
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()
105     {
106         return redirect($this->loginService->logout());
107     }
108
109     /**
110      * Get the expected username input based upon the current auth method.
111      */
112     protected function username(): string
113     {
114         return config('auth.method') === 'standard' ? 'email' : 'username';
115     }
116
117     /**
118      * Get the needed authorization credentials from the request.
119      */
120     protected function credentials(Request $request): array
121     {
122         return $request->only('username', 'email', 'password');
123     }
124
125     /**
126      * Send the response after the user was authenticated.
127      * @return RedirectResponse
128      */
129     protected function sendLoginResponse(Request $request)
130     {
131         $request->session()->regenerate();
132         $this->clearLoginAttempts($request);
133
134         return redirect()->intended('/');
135     }
136
137     /**
138      * Attempt to log the user into the application.
139      */
140     protected function attemptLogin(Request $request): bool
141     {
142         return $this->loginService->attempt(
143             $this->credentials($request),
144             auth()->getDefaultDriver(),
145             $request->filled('remember')
146         );
147     }
148
149
150     /**
151      * Validate the user login request.
152      * @throws ValidationException
153      */
154     protected function validateLogin(Request $request): void
155     {
156         $rules = ['password' => ['required', 'string']];
157         $authMethod = config('auth.method');
158
159         if ($authMethod === 'standard') {
160             $rules['email'] = ['required', 'email'];
161         }
162
163         if ($authMethod === 'ldap') {
164             $rules['username'] = ['required', 'string'];
165             $rules['email'] = ['email'];
166         }
167
168         $request->validate($rules);
169     }
170
171     /**
172      * Send a response when a login attempt exception occurs.
173      */
174     protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)
175     {
176         if ($exception instanceof LoginAttemptEmailNeededException) {
177             $request->flash();
178             session()->flash('request-email', true);
179         }
180
181         if ($message = $exception->getMessage()) {
182             $this->showWarningNotification($message);
183         }
184
185         return redirect('/login');
186     }
187
188     /**
189      * Update the intended URL location from their previous URL.
190      * Ignores if not from the current app instance or if from certain
191      * login or authentication routes.
192      */
193     protected function updateIntendedFromPrevious(): void
194     {
195         // Store the previous location for redirect after login
196         $previous = url()->previous('');
197         $isPreviousFromInstance = (strpos($previous, url('/')) === 0);
198         if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
199             return;
200         }
201
202         $ignorePrefixList = [
203             '/login',
204             '/mfa',
205         ];
206
207         foreach ($ignorePrefixList as $ignorePrefix) {
208             if (strpos($previous, url($ignorePrefix)) === 0) {
209                 return;
210             }
211         }
212
213         redirect()->setIntendedUrl($previous);
214     }
215 }