Completed
Branch feature/phpstanLevel3 (de378e)
by Schlaefer
02:30
created

AuthUserComponent::initialize()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 1
dl 0
loc 35
rs 9.36
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::getTableLocator()->get('Users');
73
74
        $this->initSessionAuth($this->Auth);
75
76
        if ($this->isBot()) {
77
            $this->CurrentUser = CurrentUserFactory::createDummy();
78
        } else {
79
            $user = $this->_login();
80
            $controller = $this->getController();
81
            $isLoggedIn = !empty($user);
82
83
            /// don't auto-login on login related pages
84
            $excluded = ['login', 'register'];
85
            $useLoggedIn = $isLoggedIn
86
                && !in_array($controller->getRequest()->getParam('action'), $excluded);
87
88
            if ($useLoggedIn) {
89
                $this->CurrentUser = CurrentUserFactory::createLoggedIn($user);
90
                $userId = $this->CurrentUser->getId();
91
            } else {
92
                $this->CurrentUser = CurrentUserFactory::createVisitor($controller);
93
                $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...
94
            }
95
96
            $this->UsersTable->UserOnline->setOnline($userId, $useLoggedIn);
97
        }
98
99
        Registry::set('CU', $this->CurrentUser);
100
101
        Stopwatch::stop('CurrentUser::initialize()');
102
    }
103
104
    /**
105
     * Detects if the current user is a bot
106
     *
107
     * @return bool
108
     */
109
    public function isBot()
110
    {
111
        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...
112
    }
113
114
    /**
115
     * Tries to authenticate a user provided by credentials in request (usually form-data)
116
     *
117
     * @return bool Was login successfull
118
     */
119
    public function login(): bool
120
    {
121
        // destroy any existing session or auth-data
122
        $this->logout();
123
124
        // non-logged in session-id is lost after Auth::setUser()
125
        $originalSessionId = session_id();
126
127
        $user = $this->_login();
128
129
        if (empty($user)) {
130
            return false;
131
        }
132
133
        $this->CurrentUser->setSettings($user);
134
        $user = $this->UsersTable->get($this->CurrentUser->getId());
135
        $this->UsersTable->incrementLogins($user);
136
        $this->UsersTable->UserOnline->setOffline($originalSessionId);
137
138
        //= password update
139
        $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...
140
        if ($password) {
141
            $this->UsersTable->autoUpdatePassword($this->CurrentUser->getId(), $password);
142
        }
143
144
        //= set persistent Cookie
145
        $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...
146
        if ($setCookie) {
147
            (new CurrentUserCookie($this->getController()))->write($this->CurrentUser->getId());
148
        };
149
150
        return true;
151
    }
152
153
    /**
154
     * Tries to login the user.
155
     *
156
     * @return null|array if user is logged-in null otherwise
157
     */
158
    protected function _login(): ?array
159
    {
160
        // Check if AuthComponent knows user from session-storage (usually
161
        // compare session-cookie)
162
        // Notice: Will hit session storage. Usually files.
163
        $user = $this->Auth->user();
164
165
        if (!$user) {
166
            // Check if user is authenticated via one of the Authenticators
167
            // (cookie, token, …).
168
            // Notice: Authenticators may hit DB to find user
169
            $user = $this->Auth->identify();
170
171
            if (!empty($user)) {
172
                // set user in session-storage to be available in subsequent requests
173
                // Notice: on write Cake 3 will start a new session (new session-id)
174
                $this->Auth->setUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->Auth->identify() on line 169 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...
175
            }
176
        }
177
178
        if (empty($user)) {
179
            // Authentication failed.
180
            return null;
181
        }
182
183
        // Session-data may be outdated. Make sure that user-data is up-to-date:
184
        // user not locked/user-type wasn't changend/… since session-storage was written.
185
        // Notice: is going to hit DB
186
        Stopwatch::start('CurrentUser read user from DB');
187
        $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...
188
            ->findAllowedToLoginById($user['id'])
189
            ->first();
190
        Stopwatch::stop('CurrentUser read user from DB');
191
192
        if (empty($user)) {
193
            //// no user allowed to login
194
            // destroy any existing (session) storage information
195
            $this->logout();
196
            // send to logout form for formal logout procedure
197
            $this->getController()->redirect(['_name' => 'logout']);
198
199
            return null;
200
        }
201
202
        return $user->toArray();
203
    }
204
205
    /**
206
     * Logs-out user: clears session data and cookies.
207
     *
208
     * @return void
209
     */
210
    public function logout(): void
211
    {
212
        if (!empty($this->CurrentUser)) {
213
            if ($this->CurrentUser->isLoggedIn()) {
214
                $this->UsersTable->UserOnline->setOffline($this->CurrentUser->getId());
215
            }
216
            $this->CurrentUser->setSettings([]);
217
        }
218
        $this->Auth->logout();
219
    }
220
221
    /**
222
     * Configures CakePHP's authentication-component
223
     *
224
     * @param AuthComponent $auth auth-component to configure
225
     * @return void
226
     */
227
    public function initSessionAuth(AuthComponent $auth): void
228
    {
229
        if ($auth->getConfig('authenticate')) {
230
            // different auth configuration already in place (e.g. API)
231
            return;
232
        };
233
234
        $auth->setConfig(
235
            'authenticate',
236
            [
237
                AuthComponent::ALL => ['finder' => 'allowedToLogin'],
238
                'Cookie',
239
                'Mlf',
240
                'Mlf2',
241
                'Form'
242
            ]
243
        );
244
245
        $auth->setConfig('authorize', ['Controller']);
246
        $auth->setConfig('loginAction', '/login');
247
248
        $here = urlencode($this->getController()->getRequest()->getRequestTarget());
249
        $auth->setConfig('unauthorizedRedirect', '/login?redirect=' . $here);
250
251
        $auth->deny();
252
        $auth->setConfig('authError', __('authentication.error'));
253
    }
254
255
    /**
256
     * {@inheritDoc}
257
     */
258
    public function shutdown(Event $event)
259
    {
260
        $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...
261
    }
262
263
    /**
264
     * Sets (or deletes) the JS-Web-Token in Cookie for access in front-end
265
     *
266
     * @param Controller $controller The controller
267
     * @return void
268
     */
269
    private function setJwtCookie(Controller $controller): void
270
    {
271
        $cookieKey = Configure::read('Session.cookie') . '-jwt';
272
        $cookie = (new Storage($controller, $cookieKey, ['http' => false, 'expire' => '+1 week']));
273
274
        $existingToken = $cookie->read();
275
276
        // user not logged-in: no JWT-cookie for you
277
        if (!$this->CurrentUser->isLoggedIn()) {
278
            if ($existingToken) {
279
                $cookie->delete();
280
            }
281
282
            return;
283
        }
284
285
        if ($existingToken) {
286
            //// check that token belongs to current-user
287
            $parts = explode('.', $existingToken);
288
            // [performance] Done every logged-in request. Don't decrypt whole token with signature.
289
            // We only make sure it exists, the auth happens elsewhere.
290
            $payload = Jwt::jsonDecode(Jwt::urlsafeB64Decode($parts[1]));
291
            if ($payload->sub === $this->CurrentUser->getId() && $payload->exp > time()) {
292
                return;
293
            }
294
        }
295
296
        // use easy to change cookieSalt to allow emergency invalidation of all existing tokens
297
        $jwtKey = Configure::read('Security.cookieSalt');
298
        // cookie expires before JWT (7 days < 14 days): JWT exp should always be valid
299
        $jwtPayload = ['sub' => $this->CurrentUser->getId(), 'exp' => time() + (86400 * 14)];
300
        $jwtToken = \Firebase\JWT\JWT::encode($jwtPayload, $jwtKey);
301
        $cookie->write($jwtToken);
302
    }
303
304
    /**
305
     * Returns the current-user
306
     *
307
     * @return CurrentUserInterface
308
     */
309
    public function getUser(): CurrentUserInterface
310
    {
311
        return $this->CurrentUser;
312
    }
313
}
314