/
rememberme_model.php
393 lines (330 loc) · 15.5 KB
/
rememberme_model.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
<?php
class Rememberme {
private $mysqli;
private $log;
/**
* Cookie settings
* @var string
*/
private $cookieName = "EMONCMS_REMEMBERME";
private $path = '/';
private $domain = "";
private $secure = false;
private $httpOnly = false;
// Number of seconds in the future the cookie and storage will expire
private $expireTime = 7776000; // 90 days
/**
* If the return from the storage was Rememberme_Storage_StorageInterface::TRIPLET_INVALID,
* this is set to true
*
* @var bool
*/
protected $lastLoginTokenWasInvalid = false;
/**
* If the login token was invalid, delete all login tokens of this user
*
* @var type
*/
protected $cleanStoredTokensOnInvalidResult = true;
const TRIPLET_FOUND = 1,
TRIPLET_NOT_FOUND = 0,
TRIPLET_INVALID = -1;
// ---------------------------------------------------------------------------------------------------------
public function __construct($mysqli)
{
$this->mysqli = $mysqli;
$this->log = new EmonLogger(__FILE__);
}
// ---------------------------------------------------------------------------------------------------------
public function setCookie($content,$expire)
{
$this->log->info("setCookie: $content $expire");
setcookie($this->cookieName,$content,$expire,$this->path,$this->domain,$this->secure,$this->httpOnly);
// Double check cookie saved correctly
if (isset($_COOKIE[$this->cookieName]) && $_COOKIE[$this->cookieName]!=$content) {
// $this->log->warn("setCookie error cookie=".$_COOKIE[$this->cookieName]." content=".$content);
// return false;
}
return true;
}
// ---------------------------------------------------------------------------------------------------------
// Check Credentials from cookie
// @return bool|string False if login was not successful, credential string if it was successful
// ---------------------------------------------------------------------------------------------------------
public function login() {
$this->log->info("login");
if (!$cookieValues = $this->getCookieValues()) {
// If the cookie is invalid
// the only thing to do is clear the cookie
// Only clear the cookie if there is content in it
if (isset($_COOKIE[$this->cookieName]) && $_COOKIE[$this->cookieName]!="") {
// Set cookie blank and force to expire
$this->setCookie("",time()-$this->expireTime);
unset($_COOKIE[$this->cookieName]);
}
$this->lastLoginTokenWasInvalid = true;
return false;
}
$loginResult = false;
switch($this->findTriplet($cookieValues)) {
case self::TRIPLET_FOUND:
// remove old triplet before creating new one, otherwise since the salt is defaulted to "" it would create
// a new triplet with the same persistentToken in DB which will cause the next findTriplet to fail (finding the incorrect one) and remove the cookie again.
$this->cleanTriplet($cookieValues);
// create new cookie and register values in db - refresh token
$cookieValues->token = $this->createToken();
$expire = time() + $this->expireTime;
if ($this->storeTriplet($cookieValues, $expire)) {
if (!$this->setCookie(implode("|",array($cookieValues->userid,$cookieValues->token,$cookieValues->persistentToken)),$expire)) {
// this should never happen
$this->log->warn("login, errors setting cookie");
}
$loginResult = $cookieValues->userid;
} else {
$loginResult = false;
}
break;
case self::TRIPLET_INVALID:
$this->setCookie("",time()-$this->expireTime);
$this->lastLoginTokenWasInvalid = true;
if($this->cleanStoredTokensOnInvalidResult) {
$this->cleanAllTriplets($cookieValues->userid);
}
break;
case self::TRIPLET_NOT_FOUND:
// Only clear the cookie if there is content in it
if (isset($_COOKIE[$this->cookieName]) && $_COOKIE[$this->cookieName]!="") {
// Set cookie blank and force to expire
$this->setCookie("",time()-$this->expireTime);
unset($_COOKIE[$this->cookieName]);
}
break;
}
return $loginResult;
}
// ---------------------------------------------------------------------------------------------------------
public function cookieIsValid($userid) {
$this->log->info("cookieIsValid");
$userid = (int) $userid;
// Fetch cookie values, if result false cookie is not valid
if (!$cookieValues = $this->getCookieValues()) return false;
// If we have a valid cookie, check for database match
$state = $this->findTriplet($cookieValues);
if ($state === self::TRIPLET_FOUND) return true;
return false;
}
// ---------------------------------------------------------------------------------------------------------
// createCookie called from user_model, login function
// @param int $userid
// @return boolean
// ---------------------------------------------------------------------------------------------------------
public function createCookie($userid)
{
$this->log->info("createCookie");
$cookieValues = new stdClass();
$cookieValues->userid = (int) $userid;
$cookieValues->token = $this->createToken();
$cookieValues->persistentToken = $this->createToken();
$expire = time() + $this->expireTime;
if (!$this->storeTriplet($cookieValues, $expire)) {
// Failure to save entry to database, will result in message to user defined in user_model
return false;
}
if (!$this->setCookie(implode("|",array($cookieValues->userid,$cookieValues->token,$cookieValues->persistentToken)),$expire)) {
// Failure to set cookie, will result in message to user defined in user_model
return false;
}
return true;
}
// ---------------------------------------------------------------------------------------------------------
// Clear cookie
// called from user_model
// result is currently unused
// ---------------------------------------------------------------------------------------------------------
public function clearCookie() {
$this->log->info("clearCookie");
// fetch and validate cookie
$cookieValues = $this->getCookieValues();
// Only clear the cookie if there is content in it
if (isset($_COOKIE[$this->cookieName]) && $_COOKIE[$this->cookieName]!="") {
// Set cookie blank and force to expire
$this->setCookie("",time()-$this->expireTime);
unset($_COOKIE[$this->cookieName]);
}
// If original cookie was invalid exit
if (!$cookieValues) return false;
$this->log->info("clearCookie call to cleanTriplet");
if (!$this->cleanTriplet($cookieValues)) return false;
return true;
}
public function getCookieName() {
return $this->cookieName;
}
public function loginTokenWasInvalid() {
return $this->lastLoginTokenWasInvalid;
}
// ---------------------------------------------------------------------------------------------------------
// Create a pseudo-random token.
// ---------------------------------------------------------------------------------------------------------
private function createToken() {
return bin2hex(random_bytes(16));
}
// ---------------------------------------------------------------------------------------------------------
private function getCookieValues()
{
// Cookie was not sent with incoming request
if(!isset($_COOKIE[$this->cookieName])) {
$ip_address = get_client_ip_env();
$this->log->info("getCookieValues: not present for: ".$ip_address);
return false;
}
if ($_COOKIE[$this->cookieName]=="") {
return false;
}
// $this->log->info($this->cookieName." ".json_encode($_COOKIE));
$cookieValueArray = explode("|", $_COOKIE[$this->cookieName], 3);
if(count($cookieValueArray) != 3) {
$this->log->warn("getCookieValues: cookie must contain 3 parts: ".count($cookieValueArray));
return false;
}
// $this->log->info("getCookieValues: ".json_encode($cookieValueArray));
// Validate
if (intval($cookieValueArray[0])!=$cookieValueArray[0]) {
$this->log->warn("getCookieValues: userid is not an integer");
return false;
}
if (preg_replace('/[^\w\s]/','',$cookieValueArray[1])!=$cookieValueArray[1]) {
$this->log->warn("getCookieValues: token is not alphanumeric");
return false;
}
if (preg_replace('/[^\w\s]/','',$cookieValueArray[2])!=$cookieValueArray[2]) {
$this->log->warn("getCookieValues: token is not alphanumeric");
return false;
}
// Create cookie value object
$cookieValues = new stdClass();
$cookieValues->userid = (int) $cookieValueArray[0];
$cookieValues->token = $cookieValueArray[1];
$cookieValues->persistentToken = $cookieValueArray[2];
return $cookieValues;
}
// ---------------------------------------------------------------------------------------------------------
private function findTriplet($cookieValues) {
//$this->log->info("findTriplet");
if (!$stmt = $stmt = $this->mysqli->prepare("SELECT token FROM rememberme WHERE userid=? AND persistentToken=? LIMIT 1")) {
$this->log->warn("findTriplet schema fail");
return self::TRIPLET_NOT_FOUND;
}
$sha1_persistentToken = sha1($cookieValues->persistentToken);
$stmt->bind_param("is",$cookieValues->userid,$sha1_persistentToken);
if (!$stmt->execute()) {
$this->log->warn("findTriplet sql fail");
}
$stmt->bind_result($sha1_token);
$stmt->fetch();
$stmt->close();
// sha1 of token match: triplet found
if ($sha1_token==sha1($cookieValues->token)) {
$this->log->info("findTriplet TRIPLET_FOUND");
return self::TRIPLET_FOUND;
// false will occur when there are no entries
} else if ($sha1_token==false) {
$this->log->info("findTriplet TRIPLET_NOT_FOUND");
return self::TRIPLET_NOT_FOUND;
// token does not match query token
} else {
$this->log->info("findTriplet TRIPLET_INVALID");
return self::TRIPLET_INVALID;
}
}
// ---------------------------------------------------------------------------------------------------------
// $cookieValues has been validated
// called from login and createCookie
// ---------------------------------------------------------------------------------------------------------
private function storeTriplet($cookieValues, $expire=0)
{
$date = date("Y-m-d H:i:s", $expire);
if (!$stmt = $this->mysqli->prepare("INSERT INTO rememberme (userid, token, persistentToken, expire) VALUES (?,?,?,?)")) {
$this->log->warn("storeTriplet schema fail");
return false;
}
$sha1_token = sha1($cookieValues->token);
$sha1_persistentToken = sha1($cookieValues->persistentToken);
$stmt->bind_param("isss",$cookieValues->userid,$sha1_token,$sha1_persistentToken,$date);
if ($stmt->execute()) {
return true;
} else {
$this->log->warn("storeTriplet sql fail");
return false;
}
$stmt->close();
}
// ---------------------------------------------------------------------------------------------------------
// Clean entry of particular cookie
// $cookieValues have been validated
// called from login and clearCookie
// ---------------------------------------------------------------------------------------------------------
private function cleanTriplet($cookieValues)
{
if (!$stmt = $this->mysqli->prepare("DELETE FROM rememberme WHERE userid=? AND persistentToken=?")) {
$this->log->warn("cleanTriplet schema fail");
return false;
}
$sha1_persistentToken = sha1($cookieValues->persistentToken);
$stmt->bind_param("is",$cookieValues->userid,$sha1_persistentToken);
if ($stmt->execute()) {
$this->log->info("cleanTriplet success");
$this->cleanExpiredTriplets($cookieValues->userid);
return true;
} else {
$this->log->warn("cleanTriplet sql fail");
return false;
}
}
// ---------------------------------------------------------------------------------------------------------
// Delete all entries for a given user
// $userid has been validated
// called from login
// ---------------------------------------------------------------------------------------------------------
private function cleanAllTriplets($userid)
{
$this->log->info("cleanAllTriplets");
$stmt = $this->mysqli->prepare("DELETE FROM rememberme WHERE userid=?");
$stmt->bind_param("i",$userid);
if ($stmt->execute()) {
return true;
} else {
$this->log->warn("cleanAllTriplets sql fail");
return false;
}
}
// ---------------------------------------------------------------------------------------------------------
// Scans through all entries for a given user to check if they have expired
// ---------------------------------------------------------------------------------------------------------
private function cleanExpiredTriplets($userid)
{
$date = date("Y-m-d H:i:s", time());
$stmt = $this->mysqli->prepare("SELECT expire FROM rememberme WHERE userid=?");
$stmt->bind_param("i",$userid);
$stmt->execute();
$stmt->bind_result($expire);
$expire_list = array();
while ($stmt->fetch()) $expire_list[] = $expire;
$stmt->close();
$overdue_count = 0;
foreach ($expire_list as $expire)
{
$seconds_overdue = time() - strtotime($expire);
if ($seconds_overdue>0) {
$overdue_count++;
$stmt = $this->mysqli->prepare("DELETE FROM rememberme WHERE userid=? AND expire=?");
$stmt->bind_param("is",$userid,$expire);
if (!$stmt->execute()) {
$this->log->warn("could not delete expired triplet $userid $expire");
}
$stmt->close();
}
}
if ($overdue_count>0) $this->log->info("Deleted $overdue_count expired");
}
}