1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | /** |
||||
6 | * Saito - The Threaded Web Forum |
||||
7 | * |
||||
8 | * @copyright Copyright (c) the Saito Project Developers |
||||
9 | * @link https://github.com/Schlaefer/Saito |
||||
10 | * @license http://opensource.org/licenses/MIT |
||||
11 | */ |
||||
12 | |||||
13 | namespace App\Controller\Component; |
||||
14 | |||||
15 | use App\Controller\AppController; |
||||
16 | use App\Model\Entity\User; |
||||
17 | use App\Model\Table\UsersTable; |
||||
18 | use Authentication\Authenticator\CookieAuthenticator; |
||||
19 | use Authentication\Controller\Component\AuthenticationComponent; |
||||
20 | use Cake\Controller\Component; |
||||
21 | use Cake\Controller\Controller; |
||||
22 | use Cake\Core\Configure; |
||||
23 | use Cake\Event\Event; |
||||
24 | use Cake\Http\Exception\ForbiddenException; |
||||
25 | use Cake\ORM\TableRegistry; |
||||
26 | use DateTimeImmutable; |
||||
27 | use Firebase\JWT\JWT; |
||||
28 | use Saito\Exception\SaitoForbiddenException; |
||||
29 | use Saito\RememberTrait; |
||||
30 | use Saito\User\Cookie\Storage; |
||||
31 | use Saito\User\CurrentUser\CurrentUser; |
||||
32 | use Saito\User\CurrentUser\CurrentUserFactory; |
||||
33 | use Saito\User\CurrentUser\CurrentUserInterface; |
||||
34 | use Stopwatch\Lib\Stopwatch; |
||||
35 | |||||
36 | /** |
||||
37 | * Authenticates the current user and bootstraps the CurrentUser information |
||||
38 | * |
||||
39 | * @property AuthenticationComponent $Authentication |
||||
40 | */ |
||||
41 | class AuthUserComponent extends Component |
||||
42 | { |
||||
43 | use RememberTrait; |
||||
44 | |||||
45 | /** |
||||
46 | * Component name |
||||
47 | * |
||||
48 | * @var string |
||||
49 | */ |
||||
50 | public $name = 'CurrentUser'; |
||||
51 | |||||
52 | /** |
||||
53 | * Component's components |
||||
54 | * |
||||
55 | * @var array |
||||
56 | */ |
||||
57 | public $components = [ |
||||
58 | 'Authentication.Authentication', |
||||
59 | ]; |
||||
60 | |||||
61 | /** |
||||
62 | * Current user |
||||
63 | * |
||||
64 | * @var CurrentUserInterface |
||||
65 | */ |
||||
66 | protected $CurrentUser; |
||||
67 | |||||
68 | /** |
||||
69 | * UsersTableInstance |
||||
70 | * |
||||
71 | * @var UsersTable |
||||
72 | */ |
||||
73 | protected $UsersTable = null; |
||||
74 | |||||
75 | /** |
||||
76 | * Array of authorized actions 'action' => 'resource' |
||||
77 | * |
||||
78 | * @var array |
||||
79 | */ |
||||
80 | private $actionAuthorizationResources = []; |
||||
81 | |||||
82 | /** |
||||
83 | * {@inheritDoc} |
||||
84 | */ |
||||
85 | public function initialize(array $config) |
||||
86 | { |
||||
87 | Stopwatch::start('CurrentUser::initialize()'); |
||||
88 | |||||
89 | /** @var UsersTable */ |
||||
90 | $UsersTable = TableRegistry::getTableLocator()->get('Users'); |
||||
91 | $this->UsersTable = $UsersTable; |
||||
92 | |||||
93 | if ($this->isBot()) { |
||||
94 | $CurrentUser = CurrentUserFactory::createDummy(); |
||||
95 | } else { |
||||
96 | $controller = $this->getController(); |
||||
97 | $request = $controller->getRequest(); |
||||
98 | |||||
99 | $user = $this->authenticate(); |
||||
100 | if (!empty($user)) { |
||||
101 | $CurrentUser = CurrentUserFactory::createLoggedIn($user->toArray()); |
||||
102 | $userId = (string)$CurrentUser->getId(); |
||||
103 | $isLoggedIn = true; |
||||
104 | } else { |
||||
105 | $CurrentUser = CurrentUserFactory::createVisitor($controller); |
||||
106 | $userId = $request->getSession()->id(); |
||||
107 | $isLoggedIn = false; |
||||
108 | } |
||||
109 | |||||
110 | $this->UsersTable->UserOnline->setOnline($userId, $isLoggedIn); |
||||
111 | } |
||||
112 | |||||
113 | $this->setCurrentUser($CurrentUser); |
||||
114 | |||||
115 | Stopwatch::stop('CurrentUser::initialize()'); |
||||
116 | } |
||||
117 | |||||
118 | /** |
||||
119 | * {@inheritDoc} |
||||
120 | */ |
||||
121 | public function startup() |
||||
122 | { |
||||
123 | if (!$this->isAuthorized($this->CurrentUser)) { |
||||
124 | throw new SaitoForbiddenException(null, ['CurrentUser' => $this->CurrentUser]); |
||||
125 | } |
||||
126 | } |
||||
127 | |||||
128 | /** |
||||
129 | * Detects if the current user is a bot |
||||
130 | * |
||||
131 | * @return bool |
||||
132 | */ |
||||
133 | public function isBot() |
||||
134 | { |
||||
135 | return $this->remember('isBot', $this->getController()->getRequest()->is('bot')); |
||||
136 | } |
||||
137 | |||||
138 | /** |
||||
139 | * Tries to log-in a user |
||||
140 | * |
||||
141 | * Call this from controllers to authenticate manually (from login-form-data). |
||||
142 | * |
||||
143 | * @return bool Was login successfull? |
||||
144 | */ |
||||
145 | public function login(): bool |
||||
146 | { |
||||
147 | // destroy any existing session or Authentication-data |
||||
148 | $this->logout(); |
||||
149 | |||||
150 | // non-logged in session-id is lost after Authentication |
||||
151 | $originalSessionId = session_id(); |
||||
152 | |||||
153 | $user = $this->authenticate(); |
||||
154 | |||||
155 | if (!$user) { |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
156 | // login failed |
||||
157 | return false; |
||||
158 | } |
||||
159 | |||||
160 | $this->Authentication->setIdentity($user); |
||||
161 | $CurrentUser = CurrentUserFactory::createLoggedIn($user->toArray()); |
||||
162 | $this->setCurrentUser($CurrentUser); |
||||
163 | |||||
164 | $this->UsersTable->incrementLogins($user); |
||||
165 | $this->UsersTable->UserOnline->setOffline($originalSessionId); |
||||
166 | |||||
167 | /// password update |
||||
168 | $password = (string)$this->request->getData('password'); |
||||
0 ignored issues
–
show
The property
Cake\Controller\Component::$request has been deprecated: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This property has been deprecated. The supplier of the class has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead. ![]() |
|||||
169 | if ($password) { |
||||
170 | $this->UsersTable->autoUpdatePassword($this->CurrentUser->getId(), $password); |
||||
171 | } |
||||
172 | |||||
173 | return true; |
||||
174 | } |
||||
175 | |||||
176 | /** |
||||
177 | * Tries to authenticate and login the user. |
||||
178 | * |
||||
179 | * @return null|User User if is logged-in, null otherwise. |
||||
180 | */ |
||||
181 | protected function authenticate(): ?User |
||||
182 | { |
||||
183 | $result = $this->Authentication->getResult(); |
||||
184 | |||||
185 | $loginFailed = !$result->isValid(); |
||||
186 | if ($loginFailed) { |
||||
187 | return null; |
||||
188 | } |
||||
189 | |||||
190 | /** @var User User is always retrieved from ORM */ |
||||
191 | $user = $result->getData(); |
||||
192 | |||||
193 | $isUnactivated = $user['activate_code'] !== 0; |
||||
194 | $isLocked = $user['user_lock'] == true; |
||||
195 | |||||
196 | if ($isUnactivated || $isLocked) { |
||||
197 | /// User isn't allowed to be logged-in |
||||
198 | // Destroy any existing (session) storage information. |
||||
199 | $this->logout(); |
||||
200 | |||||
201 | return null; |
||||
202 | } |
||||
203 | |||||
204 | $this->refreshAuthenticationProvider(); |
||||
205 | |||||
206 | return $user; |
||||
0 ignored issues
–
show
|
|||||
207 | } |
||||
208 | |||||
209 | /** |
||||
210 | * Logs-out user: clears session data and cookies. |
||||
211 | * |
||||
212 | * @return void |
||||
213 | */ |
||||
214 | public function logout(): void |
||||
215 | { |
||||
216 | if (!empty($this->CurrentUser)) { |
||||
217 | if ($this->CurrentUser->isLoggedIn()) { |
||||
218 | $this->UsersTable->UserOnline->setOffline($this->CurrentUser->getId()); |
||||
219 | } |
||||
220 | $this->setCurrentUser(CurrentUserFactory::createVisitor($this->getController())); |
||||
221 | } |
||||
222 | $this->Authentication->logout(); |
||||
223 | } |
||||
224 | |||||
225 | /** |
||||
226 | * {@inheritDoc} |
||||
227 | */ |
||||
228 | public function shutdown(Event $event) |
||||
229 | { |
||||
230 | $this->setJwtCookie($event->getSubject()); |
||||
231 | } |
||||
232 | |||||
233 | /** |
||||
234 | * Update persistent authentication providers for regular visitors. |
||||
235 | * |
||||
236 | * Users who visit somewhat regularly shall not be logged-out. |
||||
237 | * |
||||
238 | * @return void |
||||
239 | */ |
||||
240 | private function refreshAuthenticationProvider() |
||||
241 | { |
||||
242 | // Get current authentication provider |
||||
243 | $authenticationProvider = $this->Authentication |
||||
244 | ->getAuthenticationService() |
||||
245 | ->getAuthenticationProvider(); |
||||
246 | |||||
247 | // Persistent login provider is cookie based. Every time that cookie is |
||||
248 | // used for a login its expiry is pushed forward. |
||||
249 | if ($authenticationProvider instanceof CookieAuthenticator) { |
||||
250 | $controller = $this->getController(); |
||||
251 | |||||
252 | $cookieKey = $authenticationProvider->getConfig('cookie.name'); |
||||
253 | $cookie = $controller->getRequest()->getCookieCollection()->get($cookieKey); |
||||
254 | if (empty($cookieKey) || empty($cookie)) { |
||||
255 | throw new \RuntimeException( |
||||
256 | sprintf('Auth-cookie "%s" not found for refresh.', $cookieKey), |
||||
257 | 1569739698 |
||||
258 | ); |
||||
259 | } |
||||
260 | |||||
261 | $expire = $authenticationProvider->getConfig('cookie.expire'); |
||||
262 | $refreshedCookie = $cookie |
||||
263 | ->withExpiry($expire) |
||||
264 | // Can't read path from cookies, so the default would be root '/'. |
||||
265 | ->withPath($this->getController()->getRequest()->getAttribute('webroot')); |
||||
266 | |||||
267 | $response = $controller->getResponse()->withCookie($refreshedCookie); |
||||
268 | $controller->setResponse($response); |
||||
269 | } |
||||
270 | } |
||||
271 | |||||
272 | /** |
||||
273 | * Stores (or deletes) the JS-Web-Token as Cookie for access in front-end |
||||
274 | * |
||||
275 | * @param Controller $controller The controller |
||||
276 | * @return void |
||||
277 | */ |
||||
278 | private function setJwtCookie(Controller $controller): void |
||||
279 | { |
||||
280 | $expire = '+1 day'; |
||||
281 | $cookieKey = Configure::read('Session.cookie') . '-JWT'; |
||||
282 | $cookie = new Storage( |
||||
283 | $controller, |
||||
284 | $cookieKey, |
||||
285 | ['http' => false, 'expire' => $expire] |
||||
286 | ); |
||||
287 | |||||
288 | $existingToken = $cookie->read(); |
||||
289 | |||||
290 | // User not logged-in: No JWT-cookie for you! |
||||
291 | if (!$this->CurrentUser->isLoggedIn()) { |
||||
292 | if ($existingToken) { |
||||
293 | $cookie->delete(); |
||||
294 | } |
||||
295 | |||||
296 | return; |
||||
297 | } |
||||
298 | |||||
299 | if ($existingToken) { |
||||
300 | // Encoded JWT token format: <header>.<payload>.<signature> |
||||
301 | $parts = explode('.', $existingToken); |
||||
0 ignored issues
–
show
It seems like
$existingToken can also be of type array ; however, parameter $string of explode() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
302 | $payloadEncoded = $parts[1]; |
||||
303 | // [performance] Done every logged-in request. Don't decrypt whole |
||||
304 | // token with signature. We only make sure it exists, the auth |
||||
305 | // happens elsewhere. |
||||
306 | $payload = Jwt::jsonDecode(Jwt::urlsafeB64Decode($payloadEncoded)); |
||||
307 | $isCurrentUser = $payload->sub === $this->CurrentUser->getId(); |
||||
308 | // Assume expired if within the next two hours. |
||||
309 | $aboutToExpire = $payload->exp > (time() - 7200); |
||||
310 | // Token doesn't require an update if it belongs to current user and |
||||
311 | // isn't about to expire. |
||||
312 | if ($isCurrentUser && !$aboutToExpire) { |
||||
313 | return; |
||||
314 | } |
||||
315 | } |
||||
316 | |||||
317 | /// Set new token |
||||
318 | // Use easy to change cookieSalt to allow emergency invalidation of all |
||||
319 | // existing tokens. |
||||
320 | $jwtKey = Configure::read('Security.cookieSalt'); |
||||
321 | $jwtPayload = [ |
||||
322 | 'sub' => $this->CurrentUser->getId(), |
||||
323 | // Token is valid for one day. |
||||
324 | 'exp' => (new DateTimeImmutable($expire))->getTimestamp(), |
||||
325 | ]; |
||||
326 | $jwtToken = \Firebase\JWT\JWT::encode($jwtPayload, $jwtKey); |
||||
327 | $cookie->write($jwtToken); |
||||
328 | } |
||||
329 | |||||
330 | /** |
||||
331 | * Returns the current-user |
||||
332 | * |
||||
333 | * @return CurrentUserInterface |
||||
334 | */ |
||||
335 | public function getUser(): CurrentUserInterface |
||||
336 | { |
||||
337 | return $this->CurrentUser; |
||||
338 | } |
||||
339 | |||||
340 | /** |
||||
341 | * Makes the current user available throughout the application |
||||
342 | * |
||||
343 | * @param CurrentUserInterface $CurrentUser current-user to set |
||||
344 | * @return void |
||||
345 | */ |
||||
346 | private function setCurrentUser(CurrentUserInterface $CurrentUser): void |
||||
347 | { |
||||
348 | $this->CurrentUser = $CurrentUser; |
||||
349 | |||||
350 | /** @var AppController */ |
||||
351 | $controller = $this->getController(); |
||||
352 | // makes CurrentUser available in Controllers |
||||
353 | $controller->CurrentUser = $this->CurrentUser; |
||||
354 | // makes CurrentUser available as View var in templates |
||||
355 | $controller->set('CurrentUser', $this->CurrentUser); |
||||
356 | } |
||||
357 | |||||
358 | /** |
||||
359 | * The controller action will be authorized with a permission resource. |
||||
360 | * |
||||
361 | * @param string $action The controller action to authorize. |
||||
362 | * @param string $resource The permission resource token. |
||||
363 | * @return void |
||||
364 | */ |
||||
365 | public function authorizeAction(string $action, string $resource) |
||||
366 | { |
||||
367 | $this->actionAuthorizationResources[$action] = $resource; |
||||
368 | } |
||||
369 | |||||
370 | /** |
||||
371 | * Check if user is authorized to access the current action. |
||||
372 | * |
||||
373 | * @param CurrentUser $user The current user. |
||||
374 | * @return bool True if authorized False otherwise. |
||||
375 | */ |
||||
376 | private function isAuthorized(CurrentUser $user) |
||||
377 | { |
||||
378 | /// Authorize action through resource |
||||
379 | $action = $this->getController()->getRequest()->getParam('action'); |
||||
380 | if (isset($this->actionAuthorizationResources[$action])) { |
||||
381 | return $user->permission($this->actionAuthorizationResources[$action]); |
||||
382 | } |
||||
383 | |||||
384 | /// Authorize admin area |
||||
385 | $prefix = $this->request->getParam('prefix'); |
||||
0 ignored issues
–
show
The property
Cake\Controller\Component::$request has been deprecated: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This property has been deprecated. The supplier of the class has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead. ![]() |
|||||
386 | $plugin = $this->request->getParam('plugin'); |
||||
0 ignored issues
–
show
The property
Cake\Controller\Component::$request has been deprecated: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This property has been deprecated. The supplier of the class has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead. ![]() |
|||||
387 | $isAdminRoute = ($prefix && strtolower($prefix) === 'admin') |
||||
0 ignored issues
–
show
|
|||||
388 | || ($plugin && strtolower($plugin) === 'admin'); |
||||
389 | if ($isAdminRoute) { |
||||
390 | return $user->permission('saito.core.admin.backend'); |
||||
391 | } |
||||
392 | |||||
393 | return true; |
||||
394 | } |
||||
395 | } |
||||
396 |