Completed
Branch feature/currentUserRefactoring (c13c1d)
by Schlaefer
04:13
created

AuthUserComponent::isAuthorized()   A

Complexity

Conditions 6
Paths 11

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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