Completed
Branch feature/phpstanLevel2 (e9b6b0)
by Schlaefer
02:36
created

AuthUserComponent::setJwtCookie()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 1
dl 0
loc 34
rs 8.7537
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\Model\Table\UsersTable;
16
use Cake\Controller\Component;
17
use Cake\Controller\Component\AuthComponent;
18
use Cake\Controller\Controller;
19
use Cake\Core\Configure;
20
use Cake\Event\Event;
21
use Cake\ORM\TableRegistry;
22
use Firebase\JWT\JWT;
23
use Saito\App\Registry;
24
use Saito\User\Cookie\CurrentUserCookie;
25
use Saito\User\Cookie\Storage;
26
use Saito\User\CurrentUser\CurrentUserFactory;
27
use Saito\User\CurrentUser\CurrentUserInterface;
28
use Stopwatch\Lib\Stopwatch;
29
30
/**
31
 * Authenticates the current user and bootstraps the CurrentUser information
32
 *
33
 * @property AuthComponent $Auth
34
 */
35
class AuthUserComponent extends Component
36
{
37
    /**
38
     * Component name
39
     *
40
     * @var string
41
     */
42
    public $name = 'CurrentUser';
43
44
    /**
45
     * Component's components
46
     *
47
     * @var array
48
     */
49
    public $components = ['Auth', 'Cron.Cron'];
50
51
    /**
52
     * Current user
53
     *
54
     * @var CurrentUserInterface
55
     */
56
    protected $CurrentUser;
57
58
    /**
59
     * UsersTableInstance
60
     *
61
     * @var UsersTable
62
     */
63
    protected $UsersTable = null;
64
65
    /**
66
     * {@inheritDoc}
67
     */
68
    public function initialize(array $config)
69
    {
70
        Stopwatch::start('CurrentUser::initialize()');
71
72
        $this->UsersTable = TableRegistry::get('Users');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\TableRegistry::get() has been deprecated with message: 3.6.0 Use \Cake\ORM\Locator\TableLocator::get() instead.

This method 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 method will be removed from the class and what other method or class to use instead.

Loading history...
73
74
        $this->initSessionAuth($this->Auth);
75
76
        if ($this->isBot()) {
77
            $this->CurrentUser = CurrentUserFactory::createDummy();
78
        } else {
79
            $user = $this->_login();
80
            $isLoggedIn = !empty($user);
81
            $excluded = ['login', 'register']; // don't auto-login on login related pages
82
            if ($isLoggedIn && !in_array($this->request->getParam('action'), $excluded)) {
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...
83
                $this->CurrentUser = CurrentUserFactory::createLoggedIn($user);
84
                $userId = $this->CurrentUser->getId();
85
            } else {
86
                $this->CurrentUser = CurrentUserFactory::createVisitor($this->getController());
87
                $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...
88
            }
89
90
            $this->UsersTable->UserOnline->setOnline($userId, $isLoggedIn);
91
        }
92
93
        Registry::set('CU', $this->CurrentUser);
94
95
        Stopwatch::stop('CurrentUser::initialize()');
96
    }
97
98
    /**
99
     * Detects if the current user is a bot
100
     *
101
     * @return bool
102
     */
103
    public function isBot()
104
    {
105
        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...
106
    }
107
108
    /**
109
     * Tries to authenticate a user provided by credentials in request (usually form-data)
110
     *
111
     * @return bool Was login successfull
112
     */
113
    public function login(): bool
114
    {
115
        // destroy any existing session or auth-data
116
        $this->logout();
117
118
        // non-logged in session-id is lost after Auth::setUser()
119
        $originalSessionId = session_id();
120
121
        $user = $this->_login();
122
123
        if (empty($user)) {
124
            return false;
125
        }
126
127
        $this->CurrentUser->setSettings($user);
128
        $user = $this->UsersTable->get($this->CurrentUser->getId());
129
        $this->UsersTable->incrementLogins($user);
130
        $this->UsersTable->UserOnline->setOffline($originalSessionId);
131
132
        //= password update
133
        $password = $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...
134
        if ($password) {
135
            $this->UsersTable->autoUpdatePassword($this->CurrentUser->getId(), $password);
0 ignored issues
show
Bug introduced by
It seems like $password defined by $this->request->getData('password') on line 133 can also be of type array; however, App\Model\Table\UsersTable::autoUpdatePassword() does only seem to accept string, 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...
136
        }
137
138
        //= set persistent Cookie
139
        $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...
140
        if ($setCookie) {
141
            (new CurrentUserCookie($this->getController()))->write($this->CurrentUser->getId());
142
        };
143
144
        return true;
145
    }
146
147
    /**
148
     * Tries to login the user.
149
     *
150
     * @return null|array if user is logged-in null otherwise
151
     */
152
    protected function _login(): ?array
153
    {
154
        // Check if AuthComponent knows user from session-storage (usually
155
        // compare session-cookie)
156
        // Notice: Will hit session storage. Usually files.
157
        $user = $this->Auth->user();
158
159
        if (!$user) {
160
            // Check if user is authenticated via one of the Authenticators
161
            // (cookie, token, …).
162
            // Notice: Authenticators may hit DB to find user
163
            $user = $this->Auth->identify();
164
165
            if (!empty($user)) {
166
                // set user in session-storage to be available in subsequent requests
167
                // Notice: on write Cake 3 will start a new session (new session-id)
168
                $this->Auth->setUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->Auth->identify() on line 163 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...
169
            }
170
        }
171
172
        if (empty($user)) {
173
            // Authentication failed.
174
            return null;
175
        }
176
177
        // Session-data may be outdated. Make sure that user-data is up-to-date:
178
        // user not locked/user-type wasn't changend/… since session-storage was written.
179
        // Notice: is going to hit DB
180
        Stopwatch::start('CurrentUser read user from DB');
181
        $user = $this->UsersTable
0 ignored issues
show
Documentation Bug introduced by
The method findAllowedToLoginById does not exist on object<App\Model\Table\UsersTable>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
182
            ->findAllowedToLoginById($user['id'])
183
            ->first();
184
        Stopwatch::stop('CurrentUser read user from DB');
185
186
        if (empty($user)) {
187
            //// no user allowed to login
188
            // destroy any existing (session) storage information
189
            $this->logout();
190
            // send to logout form for formal logout procedure
191
            $this->getController()->redirect(['_name' => 'logout']);
192
193
            return null;
194
        }
195
196
        return $user->toArray();
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->CurrentUser->setSettings(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array|object<App\Model\Entity\User>.

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...
211
        }
212
        $this->Auth->logout();
213
    }
214
215
    /**
216
     * Configures CakePHP's authentication-component
217
     *
218
     * @param AuthComponent $auth auth-component to configure
219
     * @return void
220
     */
221
    public function initSessionAuth(AuthComponent $auth): void
222
    {
223
        if ($auth->getConfig('authenticate')) {
224
            // different auth configuration already in place (e.g. API)
225
            return;
226
        };
227
228
        $auth->setConfig(
229
            'authenticate',
230
            [
231
                AuthComponent::ALL => ['finder' => 'allowedToLogin'],
232
                'Cookie',
233
                'Mlf',
234
                'Mlf2',
235
                'Form'
236
            ]
237
        );
238
239
        $auth->setConfig('authorize', ['Controller']);
240
        $auth->setConfig('loginAction', '/login');
241
242
        $here = urlencode($this->getController()->getRequest()->getRequestTarget());
243
        $auth->setConfig('unauthorizedRedirect', '/login?redirect=' . $here);
244
245
        $auth->deny();
246
        $auth->setConfig('authError', __('authentication.error'));
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252
    public function shutdown(Event $event)
253
    {
254
        $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...
255
    }
256
257
    /**
258
     * Sets (or deletes) the JS-Web-Token in Cookie for access in front-end
259
     *
260
     * @param Controller $controller The controller
261
     * @return void
262
     */
263
    private function setJwtCookie(Controller $controller): void
264
    {
265
        $cookieKey = Configure::read('Session.cookie') . '-jwt';
266
        $cookie = (new Storage($controller, $cookieKey, ['http' => false, 'expire' => '+1 week']));
267
268
        $existingToken = $cookie->read();
269
270
        // user not logged-in: no JWT-cookie for you
271
        if (!$this->CurrentUser->isLoggedIn()) {
272
            if ($existingToken) {
273
                $cookie->delete();
274
            }
275
276
            return;
277
        }
278
279
        if ($existingToken) {
280
            //// check that token belongs to current-user
281
            $parts = explode('.', $existingToken);
282
            // [performance] Done every logged-in request. Don't decrypt whole token with signature.
283
            // We only make sure it exists, the auth happens elsewhere.
284
            $payload = Jwt::jsonDecode(Jwt::urlsafeB64Decode($parts[1]));
285
            if ($payload->sub === $this->CurrentUser->getId() && $payload->exp > time()) {
286
                return;
287
            }
288
        }
289
290
        // use easy to change cookieSalt to allow emergency invalidation of all existing tokens
291
        $jwtKey = Configure::read('Security.cookieSalt');
292
        // cookie expires before JWT (7 days < 14 days): JWT exp should always be valid
293
        $jwtPayload = ['sub' => $this->CurrentUser->getId(), 'exp' => time() + (86400 * 14)];
294
        $jwtToken = \Firebase\JWT\JWT::encode($jwtPayload, $jwtKey);
295
        $cookie->write($jwtToken);
296
    }
297
298
    /**
299
     * Returns the current-user
300
     *
301
     * @return CurrentUserInterface
302
     */
303
    public function getUser(): CurrentUserInterface
304
    {
305
        return $this->CurrentUser;
306
    }
307
}
308