Elgg  Version 3.0
PersistentLoginService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
29 
40  public function __construct(
41  Database $db,
42  \ElggSession $session,
43  \ElggCrypto $crypto,
44  array $cookie_config,
45  $cookie_token,
46  $time = null) {
47  $this->db = $db;
48  $this->session = $session;
49  $this->crypto = $crypto;
50  $this->cookie_config = $cookie_config;
51  $this->cookie_token = $cookie_token;
52 
53  $prefix = $this->db->prefix;
54  $this->table = "{$prefix}users_remember_me_cookies";
55  $this->time = is_numeric($time) ? (int) $time : time();
56  }
57 
65  public function makeLoginPersistent(\ElggUser $user) {
66  $token = $this->generateToken();
67  $hash = $this->hashToken($token);
68 
69  $this->storeHash($user, $hash);
70  $this->setCookie($token);
71  $this->setSessionToken($token);
72  }
73 
79  public function removePersistentLogin() {
80  if ($this->cookie_token) {
81  $client_hash = $this->hashToken($this->cookie_token);
82  $this->removeHash($client_hash);
83  }
84 
85  $this->setCookie("");
86  $this->setSessionToken("");
87  }
88 
97  public function handlePasswordChange(\ElggUser $subject, \ElggUser $modifier = null) {
98  $this->removeAllHashes($subject);
99  if (!$modifier || ($modifier->guid !== $subject->guid) || !$this->cookie_token) {
100  return;
101  }
102 
103  $this->makeLoginPersistent($modifier);
104  }
105 
112  public function bootSession() {
113  if (!$this->cookie_token) {
114  return null;
115  }
116 
117  // is this token good?
118  $cookie_hash = $this->hashToken($this->cookie_token);
119  $user = $this->getUserFromHash($cookie_hash);
120  if ($user) {
121  $this->setSessionToken($this->cookie_token);
122  // note: if the token is legacy, we don't both replacing it here because
123  // it will be replaced during the next request boot
124  return $user;
125  } else {
126  if ($this->isLegacyToken($this->cookie_token)) {
127  // may be attempt to brute force legacy low-entropy tokens
128  call_user_func($this->_callable_sleep, 1);
129  }
130  $this->setCookie('');
131  }
132  }
133 
141  public function replaceLegacyToken(\ElggUser $logged_in_user) {
142  if (!$this->cookie_token || !$this->isLegacyToken($this->cookie_token)) {
143  return;
144  }
145 
146  // replace user's old weaker-entropy code with new one
147  $this->removeHash($this->hashToken($this->cookie_token));
148  $this->makeLoginPersistent($logged_in_user);
149  }
150 
158  public function getUserFromHash($hash) {
159  if (!$hash) {
160  return null;
161  }
162 
163  $query = "SELECT guid
164  FROM {$this->table}
165  WHERE code = :hash";
166  $params = [
167  ':hash' => $hash,
168  ];
169  try {
170  $user_row = $this->db->getDataRow($query, null, $params);
171  } catch (\DatabaseException $e) {
172  $this->handleDbException($e);
173  return null;
174  }
175  if (empty($user_row)) {
176  return null;
177  }
178 
179  $user = call_user_func($this->_callable_get_user, $user_row->guid);
180  return $user ? $user : null;
181  }
182 
190  public function updateTokenUsage(\ElggUser $user) {
191  if (!$this->cookie_token) {
192  return null;
193  }
194 
195  // update the database record
196  $query = "UPDATE {$this->table}
197  SET timestamp = :time
198  WHERE guid = :guid
199  AND code = :hash";
200  $params = [
201  ':time' => time(),
202  ':guid' => $user->guid,
203  ':hash' => $this->hashToken($this->cookie_token),
204  ];
205  try {
206  $res = (bool) $this->db->updateData($query, false, $params);
207  if ($res) {
208  // also update the cookie lifetime client-side
209  $this->setCookie($this->cookie_token);
210  }
211  return $res;
212  } catch (\DatabaseException $e) {
213  $this->handleDbException($e);
214  }
215 
216  return false;
217  }
218 
226  public function removeExpiredTokens($time) {
227  $time = Values::normalizeTime($time);
228 
229  $expires = Values::normalizeTime($this->cookie_config['expire']);
230  $diff = $time->diff($expires);
231 
232  $time->sub($diff);
233  if ($time->getTimestamp() > time()) {
234  return false;
235  }
236 
237  $query = "DELETE FROM {$this->table}
238  WHERE timestamp < :time";
239  $params = [
240  ':time' => $time->getTimestamp(),
241  ];
242  try {
243  return (bool) $this->db->deleteData($query, $params);
244  } catch (\DatabaseException $e) {
245  $this->handleDbException($e);
246  }
247 
248  return false;
249  }
250 
259  protected function storeHash(\ElggUser $user, $hash) {
260  // This prevents inserting the same hash twice, which seems to be happening in some rare cases
261  // and for unknown reasons. See https://github.com/Elgg/Elgg/issues/8104
262  $this->removeHash($hash);
263 
264  $query = "INSERT INTO {$this->table} (code, guid, timestamp)
265  VALUES (:hash, :guid, :time)";
266  $params = [
267  ':hash' => $hash,
268  ':guid' => $user->guid,
269  ':time' => time(),
270  ];
271  try {
272  $this->db->insertData($query, $params);
273  } catch (\DatabaseException $e) {
274  $this->handleDbException($e);
275  }
276  }
277 
284  protected function removeHash($hash) {
285  $query = "DELETE FROM {$this->table}
286  WHERE code = :hash";
287  $params = [
288  ':hash' => $hash,
289  ];
290  try {
291  $this->db->deleteData($query, $params);
292  } catch (\DatabaseException $e) {
293  $this->handleDbException($e);
294  }
295  }
296 
308  if (false !== strpos($exception->getMessage(), "users_remember_me_cookies' doesn't exist")) {
309  // schema has not been updated so we swallow this exception
310  return $default;
311  }
312 
313  throw $exception;
314  }
315 
323  public function removeAllHashes(\ElggUser $user) {
324  $query = "DELETE FROM {$this->table}
325  WHERE guid = :guid";
326  $params = [
327  ':guid' => $user->guid,
328  ];
329  try {
330  $this->db->deleteData($query, $params);
331  } catch (\DatabaseException $e) {
332  $this->handleDbException($e);
333  }
334  }
335 
343  protected function hashToken($token) {
344  // note: with user passwords, you'd want legit password hashing, but since these are randomly
345  // generated and long tokens, rainbow tables aren't any help.
346  return md5($token);
347  }
348 
356  protected function setCookie($token) {
357  $cookie = new \ElggCookie($this->cookie_config['name']);
358  foreach (['expire', 'path', 'domain', 'secure', 'httponly'] as $key) {
359  $cookie->$key = $this->cookie_config[$key];
360  }
361  $cookie->value = $token;
362  if (!$token) {
363  $cookie->expire = $this->time - (86400 * 30);
364  }
365  call_user_func($this->_callable_elgg_set_cookie, $cookie);
366  }
367 
375  protected function setSessionToken($token) {
376  if ($token) {
377  $this->session->set('code', $token);
378  } else {
379  $this->session->remove('code');
380  }
381  }
382 
391  protected function generateToken() {
392  return 'z' . $this->crypto->getRandomString(31);
393  }
394 
402  protected function isLegacyToken($token) {
403  return (isset($token[0]) && $token[0] !== 'z');
404  }
405 
409  protected $db;
410 
414  protected $table;
415 
419  protected $cookie_config;
420 
424  protected $cookie_token;
425 
429  protected $session;
430 
434  protected $crypto;
435 
439  protected $time;
440 
445  public $_callable_get_user = 'get_user';
446 
451  public $_callable_elgg_set_cookie = 'elgg_set_cookie';
452 
457  public $_callable_sleep = 'sleep';
458 }
$query
Definition: groups.php:8
handleDbException(\DatabaseException $exception, $default=null)
Swallow a schema not upgraded exception, otherwise rethrow it.
$params
Saves global plugin settings.
Definition: save.php:13
removeHash($hash)
Remove a hash from the DB.
setSessionToken($token)
Store the token in the session (or remove it from the session)
makeLoginPersistent(\ElggUser $user)
Make the user&#39;s login persistent.
removeAllHashes(\ElggUser $user)
Remove all the hashes associated with a user.
bootSession()
Boot the persistent login session, possibly returning the user who should be silently logged in...
__construct(Database $db,\ElggSession $session,\ElggCrypto $crypto, array $cookie_config, $cookie_token, $time=null)
Constructor.
The Elgg database.
Definition: Database.php:21
storeHash(\ElggUser $user, $hash)
Store a hash in the DB.
setCookie($token)
Store the token in the client cookie (or remove the cookie)
getUserFromHash($hash)
Find a user with the given hash.
Configuration exception.
isLegacyToken($token)
Is the given token a legacy MD5 hash?
if(!$entity instanceof ElggEntity) $time
Definition: time.php:21
$exception
$user
Definition: ban.php:7
removeExpiredTokens($time)
Remove all persistent codes from the database which have expired based on the cookie config...
$default
Definition: checkbox.php:35
$token
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:55
generateToken()
Generate a random token (base 64 URL)
updateTokenUsage(\ElggUser $user)
Update the timestamp linked to a persistent cookie code, this indicates that the code was used recent...
hashToken($token)
Create a hash from the token.
removePersistentLogin()
Remove the persisted login token from client and server.
handlePasswordChange(\ElggUser $subject,\ElggUser $modifier=null)
Handle a password change.
replaceLegacyToken(\ElggUser $logged_in_user)
Replace the user&#39;s token if it&#39;s a legacy hexadecimal token.
$subject
Definition: useradd.php:59