Completed
Push — develop ( 419626...edd22b )
by Schlaefer
02:31
created

AuthUserComponent   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 0
Metric Value
dl 0
loc 335
rs 9.52
c 0
b 0
f 0
wmc 36
lcom 1
cbo 24

12 Methods

Rating   Name   Duplication   Size   Complexity  
A logout() 0 10 3
A initialize() 0 32 3
A startup() 0 6 2
A isBot() 0 4 1
A login() 0 30 3
A authenticate() 0 27 4
A shutdown() 0 4 1
A refreshAuthenticationProvider() 0 28 4
B setJwtCookie() 0 51 6
A getUser() 0 4 1
A setCurrentUser() 0 12 1
B isAuthorized() 0 22 7
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\App\Registry;
29
use Saito\User\Cookie\Storage;
30
use Saito\User\CurrentUser\CurrentUser;
31
use Saito\User\CurrentUser\CurrentUserFactory;
32
use Saito\User\CurrentUser\CurrentUserInterface;
33
use Stopwatch\Lib\Stopwatch;
34
35
/**
36
 * Authenticates the current user and bootstraps the CurrentUser information
37
 *
38
 * @property AuthenticationComponent $Authentication
39
 */
40
class AuthUserComponent extends Component
41
{
42
    /**
43
     * Component name
44
     *
45
     * @var string
46
     */
47
    public $name = 'CurrentUser';
48
49
    /**
50
     * Component's components
51
     *
52
     * @var array
53
     */
54
    public $components = [
55
        'Authentication.Authentication',
56
    ];
57
58
    /**
59
     * Current user
60
     *
61
     * @var CurrentUserInterface
62
     */
63
    protected $CurrentUser;
64
65
    /**
66
     * UsersTableInstance
67
     *
68
     * @var UsersTable
69
     */
70
    protected $UsersTable = null;
71
72
    /**
73
     * {@inheritDoc}
74
     */
75
    public function initialize(array $config)
76
    {
77
        Stopwatch::start('CurrentUser::initialize()');
78
79
        /** @var UsersTable */
80
        $UsersTable = TableRegistry::getTableLocator()->get('Users');
81
        $this->UsersTable = $UsersTable;
82
83
        if ($this->isBot()) {
84
            $CurrentUser = CurrentUserFactory::createDummy();
85
        } else {
86
            $controller = $this->getController();
87
            $request = $controller->getRequest();
88
89
            $user = $this->authenticate();
90
            if (!empty($user)) {
91
                $CurrentUser = CurrentUserFactory::createLoggedIn($user->toArray());
92
                $userId = (string)$CurrentUser->getId();
93
                $isLoggedIn = true;
94
            } else {
95
                $CurrentUser = CurrentUserFactory::createVisitor($controller);
96
                $userId = $request->getSession()->id();
97
                $isLoggedIn = false;
98
            }
99
100
            $this->UsersTable->UserOnline->setOnline($userId, $isLoggedIn);
101
        }
102
103
        $this->setCurrentUser($CurrentUser);
104
105
        Stopwatch::stop('CurrentUser::initialize()');
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     */
111
    public function startup()
112
    {
113
        if (!$this->isAuthorized($this->CurrentUser)) {
0 ignored issues
show
Compatibility introduced by
$this->CurrentUser of type object<Saito\User\Curren...r\CurrentUserInterface> is not a sub-type of object<Saito\User\CurrentUser\CurrentUser>. It seems like you assume a concrete implementation of the interface Saito\User\CurrentUser\CurrentUserInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
114
            throw new ForbiddenException();
115
        }
116
    }
117
118
    /**
119
     * Detects if the current user is a bot
120
     *
121
     * @return bool
122
     */
123
    public function isBot()
124
    {
125
        return $this->request->is('bot');
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Controller\Component::$request has been deprecated with message: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.

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.

Loading history...
126
    }
127
128
    /**
129
     * Tries to log-in a user
130
     *
131
     * Call this from controllers to authenticate manually (from login-form-data).
132
     *
133
     * @return bool Was login successfull?
134
     */
135
    public function login(): bool
136
    {
137
        // destroy any existing session or Authentication-data
138
        $this->logout();
139
140
        // non-logged in session-id is lost after Authentication
141
        $originalSessionId = session_id();
142
143
        $user = $this->authenticate();
144
145
        if (!$user) {
146
            // login failed
147
            return false;
148
        }
149
150
        $this->Authentication->setIdentity($user);
151
        $CurrentUser = CurrentUserFactory::createLoggedIn($user->toArray());
152
        $this->setCurrentUser($CurrentUser);
153
154
        $this->UsersTable->incrementLogins($user);
155
        $this->UsersTable->UserOnline->setOffline($originalSessionId);
156
157
        /// password update
158
        $password = (string)$this->request->getData('password');
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Controller\Component::$request has been deprecated with message: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.

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.

Loading history...
159
        if ($password) {
160
            $this->UsersTable->autoUpdatePassword($this->CurrentUser->getId(), $password);
161
        }
162
163
        return true;
164
    }
165
166
    /**
167
     * Tries to authenticate and login the user.
168
     *
169
     * @return null|User User if is logged-in, null otherwise.
170
     */
171
    protected function authenticate(): ?User
172
    {
173
        $result = $this->Authentication->getResult();
174
175
        $loginFailed = !$result->isValid();
176
        if ($loginFailed) {
177
            return null;
178
        }
179
180
        /** @var User User is always retrieved from ORM */
181
        $user = $result->getData();
182
183
        $isUnactivated = $user['activate_code'] !== 0;
184
        $isLocked = $user['user_lock'] == true;
185
186
        if ($isUnactivated || $isLocked) {
187
            /// User isn't allowed to be logged-in
188
            // Destroy any existing (session) storage information.
189
            $this->logout();
190
191
            return null;
192
        }
193
194
        $this->refreshAuthenticationProvider();
195
196
        return $user;
197
    }
198
199
    /**
200
     * Logs-out user: clears session data and cookies.
201
     *
202
     * @return void
203
     */
204
    public function logout(): void
205
    {
206
        if (!empty($this->CurrentUser)) {
207
            if ($this->CurrentUser->isLoggedIn()) {
208
                $this->UsersTable->UserOnline->setOffline($this->CurrentUser->getId());
209
            }
210
            $this->setCurrentUser(CurrentUserFactory::createVisitor($this->getController()));
211
        }
212
        $this->Authentication->logout();
213
    }
214
215
    /**
216
     * {@inheritDoc}
217
     */
218
    public function shutdown(Event $event)
219
    {
220
        $this->setJwtCookie($event->getSubject());
0 ignored issues
show
Documentation introduced by
$event->getSubject() is of type object|null, but the function expects a object<Cake\Controller\Controller>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
221
    }
222
223
    /**
224
     * Update persistent authentication providers for regular visitors.
225
     *
226
     * Users who visit somewhat regularly shall not be logged-out.
227
     *
228
     * @return void
229
     */
230
    private function refreshAuthenticationProvider()
231
    {
232
        // Get current authentication provider
233
        $authenticationProvider = $this->Authentication
234
            ->getAuthenticationService()
235
            ->getAuthenticationProvider();
236
237
        // Persistent login provider is cookie based. Every time that cookie is
238
        // used for a login its expiry is pushed forward.
239
        if ($authenticationProvider instanceof CookieAuthenticator) {
240
            $controller = $this->getController();
241
242
            $cookieKey = $authenticationProvider->getConfig('cookie.name');
243
            $cookie = $controller->getRequest()->getCookieCollection()->get($cookieKey);
244
            if (empty($cookieKey) || empty($cookie)) {
245
                throw new \RuntimeException(
246
                    sprintf('Auth-cookie "%s" not found for refresh.', $cookieKey),
247
                    1569739698
248
                );
249
            }
250
251
            $expire = $authenticationProvider->getConfig('cookie.expire');
252
            $refreshedCookie = $cookie->withExpiry($expire);
253
254
            $response = $controller->getResponse()->withCookie($refreshedCookie);
255
            $controller->setResponse($response);
256
        }
257
    }
258
259
    /**
260
     * Stores (or deletes) the JS-Web-Token as Cookie for access in front-end
261
     *
262
     * @param Controller $controller The controller
263
     * @return void
264
     */
265
    private function setJwtCookie(Controller $controller): void
266
    {
267
        $expire = '+1 day';
268
        $cookieKey = Configure::read('Session.cookie') . '-JWT';
269
        $cookie = new Storage(
270
            $controller,
271
            $cookieKey,
272
            ['http' => false, 'expire' => $expire]
273
        );
274
275
        $existingToken = $cookie->read();
276
277
        // User not logged-in: No JWT-cookie for you!
278
        if (!$this->CurrentUser->isLoggedIn()) {
279
            if ($existingToken) {
280
                $cookie->delete();
281
            }
282
283
            return;
284
        }
285
286
        if ($existingToken) {
287
            // Encoded JWT token format: <header>.<payload>.<signature>
288
            $parts = explode('.', $existingToken);
289
            $payloadEncoded = $parts[1];
290
            // [performance] Done every logged-in request. Don't decrypt whole
291
            // token with signature. We only make sure it exists, the auth
292
            // happens elsewhere.
293
            $payload = Jwt::jsonDecode(Jwt::urlsafeB64Decode($payloadEncoded));
294
            $isCurrentUser = $payload->sub === $this->CurrentUser->getId();
295
            // Assume expired if within the next two hours.
296
            $aboutToExpire = $payload->exp > (time() - 7200);
297
            // Token doesn't require an update if it belongs to current user and
298
            // isn't about to expire.
299
            if ($isCurrentUser && !$aboutToExpire) {
300
                return;
301
            }
302
        }
303
304
        /// Set new token
305
        // Use easy to change cookieSalt to allow emergency invalidation of all
306
        // existing tokens.
307
        $jwtKey = Configure::read('Security.cookieSalt');
308
        $jwtPayload = [
309
            'sub' => $this->CurrentUser->getId(),
310
            // Token is valid for one day.
311
            'exp' => (new DateTimeImmutable($expire))->getTimestamp(),
312
        ];
313
        $jwtToken = \Firebase\JWT\JWT::encode($jwtPayload, $jwtKey);
314
        $cookie->write($jwtToken);
315
    }
316
317
    /**
318
     * Returns the current-user
319
     *
320
     * @return CurrentUserInterface
321
     */
322
    public function getUser(): CurrentUserInterface
323
    {
324
        return $this->CurrentUser;
325
    }
326
327
    /**
328
     * Makes the current user available throughout the application
329
     *
330
     * @param CurrentUserInterface $CurrentUser current-user to set
331
     * @return void
332
     */
333
    private function setCurrentUser(CurrentUserInterface $CurrentUser): void
334
    {
335
        $this->CurrentUser = $CurrentUser;
336
337
        /** @var AppController */
338
        $controller = $this->getController();
339
        // makes CurrentUser available in Controllers
340
        $controller->CurrentUser = $this->CurrentUser;
341
        // makes CurrentUser available as View var in templates
342
        $controller->set('CurrentUser', $this->CurrentUser);
343
        Registry::set('CU', $this->CurrentUser);
344
    }
345
346
    /**
347
     * Check if user is authorized to access the current action.
348
     *
349
     * @param CurrentUser $user The current user.
350
     * @return bool True if authorized False otherwise.
351
     */
352
    private function isAuthorized(CurrentUser $user)
353
    {
354
        $controller = $this->getController();
355
        $action = $controller->getRequest()->getParam('action');
356
357
        if (isset($controller->actionAuthConfig)
358
            && isset($controller->actionAuthConfig[$action])) {
359
            $requiredRole = $controller->actionAuthConfig[$action];
360
361
            return $user->permission($requiredRole);
362
        }
363
364
        $prefix = $this->request->getParam('prefix');
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Controller\Component::$request has been deprecated with message: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.

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.

Loading history...
365
        $plugin = $this->request->getParam('plugin');
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Controller\Component::$request has been deprecated with message: 3.4.0 Storing references to the request is deprecated. Use Component::getController() or callback $event->getSubject() to access the controller & request instead.

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.

Loading history...
366
        $isAdminRoute = ($prefix && strtolower($prefix) === 'admin')
367
            || ($plugin && strtolower($plugin) === 'admin');
368
        if ($isAdminRoute) {
369
            return $user->permission('saito.core.admin.backend');
370
        }
371
372
        return true;
373
    }
374
}
375