1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Framy Framework |
4
|
|
|
* |
5
|
|
|
* @copyright Copyright Framy |
6
|
|
|
* @Author Marco Bier <[email protected]> |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace app\framework\Component\Auth; |
10
|
|
|
|
11
|
|
|
use app\framework\Component\Database\DB; |
12
|
|
|
use app\framework\Component\Database\Model\Model; |
13
|
|
|
use app\framework\Component\EventManager\EventManagerTrait; |
14
|
|
|
use app\framework\Component\Hashing\Hash; |
15
|
|
|
use app\framework\Component\Http\Session; |
16
|
|
|
use app\framework\Component\Routing\Request; |
17
|
|
|
use app\framework\Component\StdLib\SingletonTrait; |
18
|
|
|
use app\framework\Component\StdLib\StdObject\StringObject\StringObject; |
19
|
|
|
use app\framework\Component\StdLib\StdObject\StringObject\StringObjectException; |
20
|
|
|
use Exception; |
21
|
|
|
|
22
|
|
|
class SessionGuard |
23
|
|
|
{ |
24
|
|
|
use SingletonTrait,EventManagerTrait; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The currently authenticated user. |
28
|
|
|
*/ |
29
|
|
|
protected $user; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var Session |
33
|
|
|
*/ |
34
|
|
|
protected $session; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* The user provider implementation. |
38
|
|
|
* |
39
|
|
|
* @var UserProvider |
40
|
|
|
*/ |
41
|
|
|
protected $provider; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Indicates if the logout method has been called. |
45
|
|
|
* |
46
|
|
|
* @var bool |
47
|
|
|
*/ |
48
|
|
|
protected $loggedOut = false; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var Request |
52
|
|
|
*/ |
53
|
|
|
protected $request; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Indicates if a token user retrieval has been attempted. |
57
|
|
|
* |
58
|
|
|
* @var bool |
59
|
|
|
*/ |
60
|
|
|
protected $tokenRetrievalAttempted = false; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* |
64
|
|
|
*/ |
65
|
|
|
public function init() |
66
|
|
|
{ |
67
|
|
|
$this->session = new Session(); |
68
|
|
|
$this->provider = new UserProvider(); |
69
|
|
|
|
70
|
|
|
$this->request = Request::createFromGlobals(); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Determine if the current user is authenticated. |
75
|
|
|
* |
76
|
|
|
* @return bool |
77
|
|
|
* @throws StringObjectException |
78
|
|
|
*/ |
79
|
|
|
public function check() |
80
|
|
|
{ |
81
|
|
|
return ! is_null($this->user()); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Determine if the current user is a guest. |
86
|
|
|
* |
87
|
|
|
* @return bool |
88
|
|
|
* @throws StringObjectException |
89
|
|
|
*/ |
90
|
|
|
public function guest() |
91
|
|
|
{ |
92
|
|
|
return ! $this->check(); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Get the ID for the currently authenticated user. |
97
|
|
|
* |
98
|
|
|
* @return int|null |
99
|
|
|
* @throws StringObjectException |
100
|
|
|
*/ |
101
|
|
|
public function id() |
102
|
|
|
{ |
103
|
|
|
if ($this->user()) { |
104
|
|
|
return $this->user()->id; |
|
|
|
|
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
return null; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Update the session with the given ID. |
112
|
|
|
* |
113
|
|
|
* @param string $id |
114
|
|
|
* @return void |
115
|
|
|
*/ |
116
|
|
|
protected function updateSession($id) |
117
|
|
|
{ |
118
|
|
|
$this->session->set($this->getName(), $id); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Get the currently authenticated user. |
123
|
|
|
* @return Model|null |
124
|
|
|
* @throws StringObjectException |
125
|
|
|
*/ |
126
|
|
|
public function user() |
127
|
|
|
{ |
128
|
|
|
if ($this->loggedOut) { |
129
|
|
|
return null; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
// If we've already retrieved the user for the current request we can just |
133
|
|
|
// return it back immediately. We do not want to fetch the user data on |
134
|
|
|
// every call to this method because that would be tremendously slow. |
135
|
|
|
if (! is_null($this->user)) { |
136
|
|
|
return $this->user; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$id = $this->session->get($this->getName()); |
140
|
|
|
|
141
|
|
|
// First we will try to load the user using the identifier in the session if |
142
|
|
|
// one exists. Otherwise we will check for a "remember me" cookie in this |
143
|
|
|
// request, and if one exists, attempt to retrieve the user using that. |
144
|
|
|
$user = null; |
145
|
|
|
|
146
|
|
|
if (! is_null($id)) { |
147
|
|
|
$user = $this->provider->retrieveById($id); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
// If the user is null, but we decrypt a "recaller" cookie we can attempt to |
151
|
|
|
// pull the user data on that cookie which serves as a remember cookie on |
152
|
|
|
// the application. Once we have a user we can return it to the caller. |
153
|
|
|
$recaller = $this->getRecaller(); |
154
|
|
|
|
155
|
|
|
if (is_null($user) && ! is_null($recaller)) { |
156
|
|
|
$user = $this->getUserByRecaller($recaller); |
|
|
|
|
157
|
|
|
|
158
|
|
|
if ($user) { |
159
|
|
|
$this->updateSession($user->id); |
160
|
|
|
|
161
|
|
|
$this->fireLoginEvent($user, true); |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
return $this->user = $user; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Attempt to authenticate a user using the given credentials. |
170
|
|
|
* |
171
|
|
|
* @param array $credentials |
172
|
|
|
* @param bool $remember |
173
|
|
|
* @param bool $login |
174
|
|
|
* @return bool |
175
|
|
|
* @throws StringObjectException |
176
|
|
|
*/ |
177
|
|
|
public function attempt(array $credentials = [], bool $remember = false, $login = true): bool |
178
|
|
|
{ |
179
|
|
|
$this->fireAttemptingEvent($credentials, $remember, $login); |
180
|
|
|
|
181
|
|
|
$this->lastAttempted = $this->user = $this->provider->retrieveByCredentials($credentials); |
|
|
|
|
182
|
|
|
|
183
|
|
|
if ($this->hasValidCredentials($this->user, $credentials)) { |
184
|
|
|
if ($login) { |
185
|
|
|
$this->login($this->user, $remember); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
return true; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
return false; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* @throws Exception |
196
|
|
|
*/ |
197
|
|
|
protected function createRememberTokenIfDoesntExist() |
198
|
|
|
{ |
199
|
|
|
if (! isset($this->user->remember_token)) { |
200
|
|
|
$this->refreshRememberToken(); |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Remove the user data from the session and cookies. |
206
|
|
|
* |
207
|
|
|
* @return void |
208
|
|
|
*/ |
209
|
|
|
protected function clearUserDataFromStorage() |
210
|
|
|
{ |
211
|
|
|
$this->session->remove($this->getName()); |
212
|
|
|
|
213
|
|
|
if (! is_null($this->getRecaller())) { |
214
|
|
|
$recaller = $this->getRecallerName(); |
215
|
|
|
|
216
|
|
|
setcookie($recaller, null, -1, '/'); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* @throws Exception |
222
|
|
|
*/ |
223
|
|
|
protected function refreshRememberToken() |
224
|
|
|
{ |
225
|
|
|
$this->user->rember_token = $token = StringObject::random(60); |
226
|
|
|
|
227
|
|
|
DB::update("UPDATE users SET remember_token=:token WHERE id=:id", [ |
228
|
|
|
'token' => $token, |
229
|
|
|
'id' => $this->user->id |
230
|
|
|
]); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @param $user |
235
|
|
|
* @param $remember |
236
|
|
|
* @throws StringObjectException |
237
|
|
|
* @throws Exception |
238
|
|
|
*/ |
239
|
|
|
public function login($user, $remember) |
240
|
|
|
{ |
241
|
|
|
$this->session->set($this->getName(), $user->id); |
242
|
|
|
|
243
|
|
|
if ($remember) { |
244
|
|
|
$this->createRememberTokenIfDoesntExist(); |
245
|
|
|
|
246
|
|
|
$value = $user->id."|".$user->remember_token; |
247
|
|
|
setcookie("remember_session_".sha1(get_class($this)), $value); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$this->fireLoginEvent($user, $remember); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Logout authenticated user |
255
|
|
|
* |
256
|
|
|
* @throws Exception |
257
|
|
|
*/ |
258
|
|
|
public function logout() |
259
|
|
|
{ |
260
|
|
|
// If we have an event dispatcher instance, we can fire off the logout event |
261
|
|
|
// so any further processing can be done. This allows the developer to be |
262
|
|
|
// listening for anytime a user signs out of this application manually. |
263
|
|
|
$this->clearUserDataFromStorage(); |
264
|
|
|
|
265
|
|
|
if (! is_null($this->user)) { |
266
|
|
|
$this->refreshRememberToken(); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
//TODO: fire logout event |
270
|
|
|
|
271
|
|
|
// Once we have fired the logout event we will clear the users out of memory |
272
|
|
|
// so they are no longer available as the user is no longer considered as |
273
|
|
|
// being signed into this application and should not be available here. |
274
|
|
|
$this->user = null; |
275
|
|
|
|
276
|
|
|
$this->loggedOut = true; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @param array $credentials |
281
|
|
|
* @param bool $remember |
282
|
|
|
* @param bool $login |
283
|
|
|
* @throws StringObjectException |
284
|
|
|
*/ |
285
|
|
|
protected function fireAttemptingEvent(array $credentials = [], bool $remember = false, $login = true) |
286
|
|
|
{ |
287
|
|
|
$this->eventManager()->fire("auth.attempting", [ |
288
|
|
|
'credentials' => $credentials, |
289
|
|
|
'remember' => $remember, |
290
|
|
|
'login' => $login |
291
|
|
|
]); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* @param $user |
296
|
|
|
* @param $remember |
297
|
|
|
* @throws StringObjectException |
298
|
|
|
*/ |
299
|
|
|
protected function fireLoginEvent($user, $remember) |
300
|
|
|
{ |
301
|
|
|
$this->eventManager()->fire("auth.login", [ |
302
|
|
|
'user' => $user, |
303
|
|
|
'remember' => $remember, |
304
|
|
|
]); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* @param $recaller |
309
|
|
|
* @return Model|null |
310
|
|
|
* @throws StringObjectException |
311
|
|
|
*/ |
312
|
|
|
public function getUserByRecaller($recaller) |
313
|
|
|
{ |
314
|
|
|
if ($this->validRecaller($recaller) && ! $this->tokenRetrievalAttempted) { |
315
|
|
|
$this->tokenRetrievalAttempted = true; |
316
|
|
|
|
317
|
|
|
list($id, $token) = explode('|', $recaller, 2); |
318
|
|
|
|
319
|
|
|
$this->viaRemember = ! is_null($user = $this->provider->retrieveByToken($id, $token)); |
|
|
|
|
320
|
|
|
|
321
|
|
|
return $user; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
return null; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Get a unique identifier for the auth session value. |
329
|
|
|
* |
330
|
|
|
* @return string |
331
|
|
|
*/ |
332
|
|
|
public function getName() |
333
|
|
|
{ |
334
|
|
|
return 'login_session_'.sha1(get_class($this)); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* @return mixed |
339
|
|
|
*/ |
340
|
|
|
public function getRecaller() |
341
|
|
|
{ |
342
|
|
|
return $this->request->cookies()->get($this->getRecallerName()); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Get the name of the cookie used to store the "recaller". |
347
|
|
|
* |
348
|
|
|
* @return string |
349
|
|
|
*/ |
350
|
|
|
public function getRecallerName() |
351
|
|
|
{ |
352
|
|
|
return 'remember_session_'.sha1(get_class($this)); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Check if user credentials are valid |
357
|
|
|
* |
358
|
|
|
* @param $user |
359
|
|
|
* @param $credentials |
360
|
|
|
* @return bool |
361
|
|
|
*/ |
362
|
|
|
protected function hasValidCredentials($user, $credentials) |
363
|
|
|
{ |
364
|
|
|
return !is_null($user) && Hash::check($credentials['password'], $user->password); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Determine if the recaller cookie is in a valid format. |
369
|
|
|
* |
370
|
|
|
* @param mixed $recaller |
371
|
|
|
* @throws StringObjectException |
372
|
|
|
* @return bool |
373
|
|
|
*/ |
374
|
|
|
protected function validRecaller($recaller) |
375
|
|
|
{ |
376
|
|
|
if (! is_string($recaller) || ! (new StringObject($recaller))->contains('|')) { |
377
|
|
|
return false; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
$segments = explode('|', $recaller); |
381
|
|
|
|
382
|
|
|
return count($segments) == 2 && trim($segments[0]) !== '' && trim($segments[1]) !== ''; |
383
|
|
|
} |
384
|
|
|
} |
385
|
|
|
|