Completed
Push — master ( fd5325...d7e193 )
by Schlaefer
05:54 queued 03:00
created

AuthUserComponent::isBot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
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 = (string)$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). This is
243
            // important for the JWT-request, so that we don't authenticate via
244
            // Cookie and open up for xsrf issues.
245
            return;
246
        };
247
248
        $auth->setConfig(
249
            'authenticate',
250
            [
251
                AuthComponent::ALL => ['finder' => 'allowedToLogin'],
252
                'Cookie',
253
                'Mlf',
254
                'Mlf2',
255
                'Form'
256
            ]
257
        );
258
259
        $auth->setConfig('authorize', ['Controller']);
260
        $auth->setConfig('loginAction', '/login');
261
262
        $here = urlencode($this->getController()->getRequest()->getRequestTarget());
263
        $auth->setConfig('unauthorizedRedirect', '/login?redirect=' . $here);
264
265
        $auth->deny();
266
        $auth->setConfig('authError', __('authentication.error'));
267
    }
268
269
    /**
270
     * {@inheritDoc}
271
     */
272
    public function shutdown(Event $event)
273
    {
274
        $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...
275
    }
276
277
    /**
278
     * Sets (or deletes) the JS-Web-Token in Cookie for access in front-end
279
     *
280
     * @param Controller $controller The controller
281
     * @return void
282
     */
283
    private function setJwtCookie(Controller $controller): void
284
    {
285
        $cookieKey = Configure::read('Session.cookie') . '-jwt';
286
        $cookie = (new Storage($controller, $cookieKey, ['http' => false, 'expire' => '+1 week']));
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
            //// check that token belongs to current-user
301
            $parts = explode('.', $existingToken);
302
            // [performance] Done every logged-in request. Don't decrypt whole token with signature.
303
            // We only make sure it exists, the auth happens elsewhere.
304
            $payload = Jwt::jsonDecode(Jwt::urlsafeB64Decode($parts[1]));
305
            if ($payload->sub === $this->CurrentUser->getId() && $payload->exp > time()) {
306
                return;
307
            }
308
        }
309
310
        // use easy to change cookieSalt to allow emergency invalidation of all existing tokens
311
        $jwtKey = Configure::read('Security.cookieSalt');
312
        // cookie expires before JWT (7 days < 14 days): JWT exp should always be valid
313
        $jwtPayload = ['sub' => $this->CurrentUser->getId(), 'exp' => time() + (86400 * 14)];
314
        $jwtToken = \Firebase\JWT\JWT::encode($jwtPayload, $jwtKey);
315
        $cookie->write($jwtToken);
316
    }
317
318
    /**
319
     * Returns the current-user
320
     *
321
     * @return CurrentUserInterface
322
     */
323
    public function getUser(): CurrentUserInterface
324
    {
325
        return $this->CurrentUser;
326
    }
327
328
    /**
329
     * Makes the current user available throughout the application
330
     *
331
     * @param CurrentUserInterface $CurrentUser current-user to set
332
     * @return void
333
     */
334
    private function setCurrentUser(CurrentUserInterface $CurrentUser): void
335
    {
336
        $this->CurrentUser = $CurrentUser;
337
338
        /** @var AppController */
339
        $controller = $this->getController();
340
        // makes CurrentUser available in Controllers
341
        $controller->CurrentUser = $this->CurrentUser;
342
        // makes CurrentUser available as View var in templates
343
        $controller->set('CurrentUser', $this->CurrentUser);
344
        Registry::set('CU', $this->CurrentUser);
345
    }
346
}
347