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(); |
|
|
|
|
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'); |
|
|
|
|
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'); |
|
|
|
|
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'); |
|
|
|
|
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); |
|
|
|
|
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 |
|
|
|
|
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()); |
|
|
|
|
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
|
|
|
|
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.