]> BookStack Code Mirror - bookstack/blob - app/Access/Controllers/LoginController.php
OIDC: Update example env option to reflect correct default
[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\Validation\ValidationException;
14
15 class LoginController extends Controller
16 {
17     use ThrottlesLogins;
18
19     public function __construct(
20         protected SocialDriverManager $socialDriverManager,
21         protected LoginService $loginService,
22     ) {
23         $this->middleware('guest', ['only' => ['getLogin', 'login']]);
24         $this->middleware('guard:standard,ldap', ['only' => ['login']]);
25         $this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
26     }
27
28     /**
29      * Show the application login form.
30      */
31     public function getLogin(Request $request)
32     {
33         $socialDrivers = $this->socialDriverManager->getActive();
34         $authMethod = config('auth.method');
35         $preventInitiation = $request->get('prevent_auto_init') === 'true';
36
37         if ($request->has('email')) {
38             session()->flashInput([
39                 'email'    => $request->get('email'),
40                 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
41             ]);
42         }
43
44         // Store the previous location for redirect after login
45         $this->updateIntendedFromPrevious();
46
47         if (!$preventInitiation && $this->loginService->shouldAutoInitiate()) {
48             return view('auth.login-initiate', [
49                 'authMethod'    => $authMethod,
50             ]);
51         }
52
53         return view('auth.login', [
54             'socialDrivers' => $socialDrivers,
55             'authMethod'    => $authMethod,
56         ]);
57     }
58
59     /**
60      * Handle a login request to the application.
61      */
62     public function login(Request $request)
63     {
64         $this->validateLogin($request);
65         $username = $request->get($this->username());
66
67         // Check login throttling attempts to see if they've gone over the limit
68         if ($this->hasTooManyLoginAttempts($request)) {
69             Activity::logFailedLogin($username);
70             return $this->sendLockoutResponse($request);
71         }
72
73         try {
74             if ($this->attemptLogin($request)) {
75                 return $this->sendLoginResponse($request);
76             }
77         } catch (LoginAttemptException $exception) {
78             Activity::logFailedLogin($username);
79
80             return $this->sendLoginAttemptExceptionResponse($exception, $request);
81         }
82
83         // On unsuccessful login attempt, Increment login attempts for throttling and log failed login.
84         $this->incrementLoginAttempts($request);
85         Activity::logFailedLogin($username);
86
87         // Throw validation failure for failed login
88         throw ValidationException::withMessages([
89             $this->username() => [trans('auth.failed')],
90         ])->redirectTo('/login');
91     }
92
93     /**
94      * Logout user and perform subsequent redirect.
95      */
96     public function logout()
97     {
98         return redirect($this->loginService->logout());
99     }
100
101     /**
102      * Get the expected username input based upon the current auth method.
103      */
104     protected function username(): string
105     {
106         return config('auth.method') === 'standard' ? 'email' : 'username';
107     }
108
109     /**
110      * Get the needed authorization credentials from the request.
111      */
112     protected function credentials(Request $request): array
113     {
114         return $request->only('username', 'email', 'password');
115     }
116
117     /**
118      * Send the response after the user was authenticated.
119      * @return RedirectResponse
120      */
121     protected function sendLoginResponse(Request $request)
122     {
123         $request->session()->regenerate();
124         $this->clearLoginAttempts($request);
125
126         return redirect()->intended('/');
127     }
128
129     /**
130      * Attempt to log the user into the application.
131      */
132     protected function attemptLogin(Request $request): bool
133     {
134         return $this->loginService->attempt(
135             $this->credentials($request),
136             auth()->getDefaultDriver(),
137             $request->filled('remember')
138         );
139     }
140
141
142     /**
143      * Validate the user login request.
144      * @throws ValidationException
145      */
146     protected function validateLogin(Request $request): void
147     {
148         $rules = ['password' => ['required', 'string']];
149         $authMethod = config('auth.method');
150
151         if ($authMethod === 'standard') {
152             $rules['email'] = ['required', 'email'];
153         }
154
155         if ($authMethod === 'ldap') {
156             $rules['username'] = ['required', 'string'];
157             $rules['email'] = ['email'];
158         }
159
160         $request->validate($rules);
161     }
162
163     /**
164      * Send a response when a login attempt exception occurs.
165      */
166     protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)
167     {
168         if ($exception instanceof LoginAttemptEmailNeededException) {
169             $request->flash();
170             session()->flash('request-email', true);
171         }
172
173         if ($message = $exception->getMessage()) {
174             $this->showWarningNotification($message);
175         }
176
177         return redirect('/login');
178     }
179
180     /**
181      * Update the intended URL location from their previous URL.
182      * Ignores if not from the current app instance or if from certain
183      * login or authentication routes.
184      */
185     protected function updateIntendedFromPrevious(): void
186     {
187         // Store the previous location for redirect after login
188         $previous = url()->previous('');
189         $isPreviousFromInstance = str_starts_with($previous, url('/'));
190         if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
191             return;
192         }
193
194         $ignorePrefixList = [
195             '/login',
196             '/mfa',
197         ];
198
199         foreach ($ignorePrefixList as $ignorePrefix) {
200             if (str_starts_with($previous, url($ignorePrefix))) {
201                 return;
202             }
203         }
204
205         redirect()->setIntendedUrl($previous);
206     }
207 }