Completed
Push — develop ( fed724...263ba5 )
by Schlaefer
03:00 queued 11s
created

AuthUserComponent::initialize()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 1
dl 0
loc 37
rs 9.328
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\Table\UsersTable;
17
use Cake\Controller\Component;
18
use Cake\Controller\Component\AuthComponent;
19
use Cake\Controller\Controller;
20
use Cake\Core\Configure;
21
use Cake\Event\Event;
22
use Cake\ORM\TableRegistry;
23
use Firebase\JWT\JWT;
24
use Saito\App\Registry;
25
use Saito\User\Cookie\CurrentUserCookie;
26
use Saito\User\Cookie\Storage;
27
use Saito\User\CurrentUser\CurrentUserFactory;
28
use Saito\User\CurrentUser\CurrentUserInterface;
29
use Stopwatch\Lib\Stopwatch;
30
31
/**
32
 * Authenticates the current user and bootstraps the CurrentUser information
33
 *
34
 * @property AuthComponent $Auth
35
 */
36
class AuthUserComponent extends Component
37
{
38
    /**
39
     * Component name
40
     *
41
     * @var string
42
     */
43
    public $name = 'CurrentUser';
44
45
    /**
46
     * Component's components
47
     *
48
     * @var array
49
     */
50
    public $components = ['Auth', 'Cron.Cron'];
51
52
    /**
53
     * Current user
54
     *
55
     * @var CurrentUserInterface
56
     */
57
    protected $CurrentUser;
58
59
    /**
60
     * UsersTableInstance
61
     *
62
     * @var UsersTable
63
     */
64
    protected $UsersTable = null;
65
66
    /**
67
     * {@inheritDoc}
68
     */
69
    public function initialize(array $config)
70
    {
71
        Stopwatch::start('CurrentUser::initialize()');
72
73
        /** @var UsersTable */
74
        $UsersTable = TableRegistry::getTableLocator()->get('Users');
75
        $this->UsersTable = $UsersTable;
76
77
        $this->initSessionAuth($this->Auth);
78
79
        if ($this->isBot()) {
80
            $CurrentUser = CurrentUserFactory::createDummy();
81
        } else {
82
            $user = $this->_login();
83
            $controller = $this->getController();
84
            $isLoggedIn = !empty($user);
85
86
            /// don't auto-login on login related pages
87
            $excluded = ['login', 'register'];
88
            $useLoggedIn = $isLoggedIn
89
                && !in_array($controller->getRequest()->getParam('action'), $excluded);
90
91
            if ($useLoggedIn) {
92
                $CurrentUser = CurrentUserFactory::createLoggedIn($user);
93
                $userId = $CurrentUser->getId();
94
            } else {
95
                $CurrentUser = CurrentUserFactory::createVisitor($controller);
96
                $userId = $this->request->getSession()->id();
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...
97
            }
98
99
            $this->UsersTable->UserOnline->setOnline($userId, $useLoggedIn);
100
        }
101
102
        $this->setCurrentUser($CurrentUser);
103
104
        Stopwatch::stop('CurrentUser::initialize()');
105
    }
106
107
    /**
108
     * Detects if the current user is a bot
109
     *
110
     * @return bool
111
     */
112
    public function isBot()
113
    {
114
        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...
115
    }
116
117
    /**
118
     * Tries to log-in a user
119
     *
120
     * Call this from controllers to authenticate manually (from login-form-data).
121
     *
122
     * @return bool Was login successfull?
123
     */
124
    public function login(): bool
125
    {
126
        // destroy any existing session or auth-data
127
        $this->logout();
128
129
        // non-logged in session-id is lost after Auth::setUser()
130
        $originalSessionId = session_id();
131
132
        $user = $this->_login();
133
134
        if (empty($user)) {
135
            // login failed
136
            return false;
137
        }
138
139
        //// Login succesfull
140
141
        $user = $this->UsersTable->get($user['id']);
142
143
        $CurrentUser = CurrentUserFactory::createLoggedIn($user->toArray());
144
        $this->setCurrentUser($CurrentUser);
145
146
        $this->UsersTable->incrementLogins($user);
147
        $this->UsersTable->UserOnline->setOffline($originalSessionId);
148
149
        /// password update
150
        $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...
151
        if ($password) {
152
            $this->UsersTable->autoUpdatePassword($this->CurrentUser->getId(), $password);
153
        }
154
155
        /// set persistent Cookie
156
        $setCookie = (bool)$this->request->getData('remember_me');
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...
157
        if ($setCookie) {
158
            (new CurrentUserCookie($this->getController()))->write($this->CurrentUser->getId());
159
        };
160
161
        return true;
162
    }
163
164
    /**
165
     * Tries to login the user.
166
     *
167
     * @return null|array if user is logged-in null otherwise
168
     */
169
    protected function _login(): ?array
170
    {
171
        // Check if AuthComponent knows user from session-storage (usually
172
        // compare session-cookie)
173
        // Notice: Will hit session storage. Usually files.
174
        $user = $this->Auth->user();
175
176
        if (!$user) {
177
            // Check if user is authenticated via one of the Authenticators
178
            // (cookie, token, …).
179
            // Notice: Authenticators may hit DB to find user
180
            $user = $this->Auth->identify();
181
182
            if (!empty($user)) {
183
                // set user in session-storage to be available in subsequent requests
184
                // Notice: on write Cake 3 will start a new session (new session-id)
185
                $this->Auth->setUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->Auth->identify() on line 180 can also be of type boolean; however, Cake\Controller\Component\AuthComponent::setUser() does only seem to accept array|object<ArrayAccess>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
186
            }
187
        }
188
189
        if (empty($user)) {
190
            // Authentication failed.
191
            return null;
192
        }
193
194
        // Session-data may be outdated. Make sure that user-data is up-to-date:
195
        // user not locked/user-type wasn't changend/… since session-storage was written.
196
        // Notice: is going to hit DB
197
        Stopwatch::start('CurrentUser read user from DB');
198
        $user = $this->UsersTable
199
            ->find('allowedToLogin')
200
            ->where(['id' => $user['id']])
201
            ->first();
202
        Stopwatch::stop('CurrentUser read user from DB');
203
204
        if (empty($user)) {
205
            /// no user allowed to login
206
            // destroy any existing (session) storage information
207
            $this->logout();
208
            // send to logout form for formal logout procedure
209
            $this->getController()->redirect(['_name' => 'logout']);
210
211
            return null;
212
        }
213
214
        return $user->toArray();
215
    }
216
217
    /**
218
     * Logs-out user: clears session data and cookies.
219
     *
220
     * @return void
221
     */
222
    public function logout(): void
223
    {
224
        if (!empty($this->CurrentUser)) {
225
            if ($this->CurrentUser->isLoggedIn()) {
226
                $this->UsersTable->UserOnline->setOffline($this->CurrentUser->getId());
227
            }
228
            $this->setCurrentUser(CurrentUserFactory::createVisitor($this->getController()));
229
        }
230
        $this->Auth->logout();
231
    }
232
233
    /**
234
     * Configures CakePHP's authentication-component
235
     *
236
     * @param AuthComponent $auth auth-component to configure
237
     * @return void
238
     */
239
    public function initSessionAuth(AuthComponent $auth): void
240
    {
241
        if ($auth->getConfig('authenticate')) {
242
            // different auth configuration already in place (e.g. API)
243
            return;
244
        };
245
246
        $auth->setConfig(
247
            'authenticate',
248
            [
249
                AuthComponent::ALL => ['finder' => 'allowedToLogin'],
250
                'Cookie',
251
                'Mlf',
252
                'Mlf2',
253
                'Form'
254
            ]
255
        );
256
257
        $auth->setConfig('authorize', ['Controller']);
258
        $auth->setConfig('loginAction', '/login');
259
260
        $here = urlencode($this->getController()->getRequest()->getRequestTarget());
261
        $auth->setConfig('unauthorizedRedirect', '/login?redirect=' . $here);
262
263
        $auth->deny();
264
        $auth->setConfig('authError', __('authentication.error'));
265
    }
266
267
    /**
268
     * {@inheritDoc}
269
     */
270
    public function shutdown(Event $event)
271
    {
272
        $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...
273
    }
274
275
    /**
276
     * Sets (or deletes) the JS-Web-Token in Cookie for access in front-end
277
     *
278
     * @param Controller $controller The controller
279
     * @return void
280
     */
281
    private function setJwtCookie(Controller $controller): void
282
    {
283
        $cookieKey = Configure::read('Session.cookie') . '-jwt';
284
        $cookie = (new Storage($controller, $cookieKey, ['http' => false, 'expire' => '+1 week']));
285
286
        $existingToken = $cookie->read();
287
288
        // user not logged-in: no JWT-cookie for you
289
        if (!$this->CurrentUser->isLoggedIn()) {
290
            if ($existingToken) {
291
                $cookie->delete();
292
            }
293
294
            return;
295
        }
296
297
        if ($existingToken) {
298
            //// check that token belongs to current-user
299
            $parts = explode('.', $existingToken);
300
            // [performance] Done every logged-in request. Don't decrypt whole token with signature.
301
            // We only make sure it exists, the auth happens elsewhere.
302
            $payload = Jwt::jsonDecode(Jwt::urlsafeB64Decode($parts[1]));
303
            if ($payload->sub === $this->CurrentUser->getId() && $payload->exp > time()) {
304
                return;
305
            }
306
        }
307
308
        // use easy to change cookieSalt to allow emergency invalidation of all existing tokens
309
        $jwtKey = Configure::read('Security.cookieSalt');
310
        // cookie expires before JWT (7 days < 14 days): JWT exp should always be valid
311
        $jwtPayload = ['sub' => $this->CurrentUser->getId(), 'exp' => time() + (86400 * 14)];
312
        $jwtToken = \Firebase\JWT\JWT::encode($jwtPayload, $jwtKey);
313
        $cookie->write($jwtToken);
314
    }
315
316
    /**
317
     * Returns the current-user
318
     *
319
     * @return CurrentUserInterface
320
     */
321
    public function getUser(): CurrentUserInterface
322
    {
323
        return $this->CurrentUser;
324
    }
325
326
    /**
327
     * Makes the current user available throughout the application
328
     *
329
     * @param CurrentUserInterface $CurrentUser current-user to set
330
     * @return void
331
     */
332
    private function setCurrentUser(CurrentUserInterface $CurrentUser): void
333
    {
334
        $this->CurrentUser = $CurrentUser;
335
336
        /** @var AppController */
337
        $controller = $this->getController();
338
        // makes CurrentUser available in Controllers
339
        $controller->CurrentUser = $this->CurrentUser;
340
        // makes CurrentUser available as View var in templates
341
        $controller->set('CurrentUser', $this->CurrentUser);
342
        Registry::set('CU', $this->CurrentUser);
343
    }
344
}
345