1 <?php
  2 
  3 /**
  4  * @package redaxo\core\login
  5  */
  6 class rex_login
  7 {
  8     protected $DB = 1;
  9     protected $sessionDuration;
 10     protected $loginQuery;
 11     protected $userQuery;
 12     protected $impersonateQuery;
 13     protected $systemId = 'default';
 14     protected $userLogin;
 15     protected $userPassword;
 16     protected $logout = false;
 17     protected $idColumn = 'id';
 18     protected $passwordColumn = 'password';
 19     protected $cache = false;
 20     protected $loginStatus = 0; // 0 = noch checken, 1 = ok, -1 = not ok
 21     protected $message = '';
 22 
 23     /** @var rex_sql */
 24     protected $user;
 25 
 26     /** @var rex_sql */
 27     protected $impersonator;
 28 
 29     /**
 30      * Constructor.
 31      */
 32     public function __construct()
 33     {
 34         self::startSession();
 35     }
 36 
 37     /**
 38      * Setzt, ob die Ergebnisse der Login-Abfrage
 39      * pro Seitenaufruf gecached werden sollen.
 40      */
 41     public function setCache($status = true)
 42     {
 43         $this->cache = $status;
 44     }
 45 
 46     /**
 47      * Setzt die Id der zu verwendenden SQL Connection.
 48      */
 49     public function setSqlDb($DB)
 50     {
 51         $this->DB = $DB;
 52     }
 53 
 54     /**
 55      * Setzt eine eindeutige System Id, damit mehrere
 56      * Sessions auf der gleichen Domain unterschieden werden können.
 57      */
 58     public function setSystemId($system_id)
 59     {
 60         $this->systemId = $system_id;
 61     }
 62 
 63     /**
 64      * Setzt das Session Timeout.
 65      */
 66     public function setSessionDuration($sessionDuration)
 67     {
 68         $this->sessionDuration = $sessionDuration;
 69     }
 70 
 71     /**
 72      * Setzt den Login und das Password.
 73      */
 74     public function setLogin($login, $password, $isPreHashed = false)
 75     {
 76         $this->userLogin = $login;
 77         $this->userPassword = $isPreHashed ? $password : sha1($password);
 78     }
 79 
 80     /**
 81      * Markiert die aktuelle Session als ausgeloggt.
 82      */
 83     public function setLogout($logout)
 84     {
 85         $this->logout = $logout;
 86     }
 87 
 88     /**
 89      * Prüft, ob die aktuelle Session ausgeloggt ist.
 90      */
 91     public function isLoggedOut()
 92     {
 93         return $this->logout;
 94     }
 95 
 96     /**
 97      * Setzt den UserQuery.
 98      *
 99      * Dieser wird benutzt, um einen bereits eingeloggten User
100      * im Verlauf seines Aufenthaltes auf der Webseite zu verifizieren
101      */
102     public function setUserQuery($user_query)
103     {
104         $this->userQuery = $user_query;
105     }
106 
107     /**
108      * Setzt den ImpersonateQuery.
109      *
110      * Dieser wird benutzt, um den User abzurufen, dessen Identität ein Admin einnehmen möchte.
111      */
112     public function setImpersonateQuery($impersonateQuery)
113     {
114         $this->impersonateQuery = $impersonateQuery;
115     }
116 
117     /**
118      * Setzt den LoginQuery.
119      *
120      * Dieser wird benutzt, um den eigentlichne Loginvorgang durchzuführen.
121      * Hier wird das eingegebene Password und der Login eingesetzt.
122      */
123     public function setLoginQuery($login_query)
124     {
125         $this->loginQuery = $login_query;
126     }
127 
128     /**
129      * Setzt den Namen der Spalte, der die User-Id enthält.
130      */
131     public function setIdColumn($idColumn)
132     {
133         $this->idColumn = $idColumn;
134     }
135 
136     /**
137      * Sets the password column.
138      *
139      * @param string $passwordColumn
140      */
141     public function setPasswordColumn($passwordColumn)
142     {
143         $this->passwordColumn = $passwordColumn;
144     }
145 
146     /**
147      * Setzt einen Meldungstext.
148      */
149     protected function setMessage($message)
150     {
151         $this->message = $message;
152     }
153 
154     /**
155      * Returns the message.
156      *
157      * @return string
158      */
159     public function getMessage()
160     {
161         return $this->message;
162     }
163 
164     /**
165      * Prüft die mit setLogin() und setPassword() gesetzten Werte
166      * anhand des LoginQueries/UserQueries und gibt den Status zurück.
167      *
168      * Gibt true zurück bei erfolg, sonst false
169      */
170     public function checkLogin()
171     {
172         // wenn logout dann header schreiben und auf error seite verweisen
173         // message schreiben
174 
175         $ok = false;
176 
177         if (!$this->logout) {
178             // LoginStatus: 0 = noch checken, 1 = ok, -1 = not ok
179 
180             // gecachte ausgabe erlaubt ? checkLogin schonmal ausgeführt ?
181             if ($this->cache && $this->loginStatus != 0) {
182                 return $this->loginStatus > 0;
183             }
184 
185             if ($this->userLogin != '') {
186                 // wenn login daten eingegeben dann checken
187                 // auf error seite verweisen und message schreiben
188 
189                 $this->user = rex_sql::factory($this->DB);
190 
191                 $this->user->setQuery($this->loginQuery, [':login' => $this->userLogin]);
192                 if ($this->user->getRows() == 1 && self::passwordVerify($this->userPassword, $this->user->getValue($this->passwordColumn), true)) {
193                     $ok = true;
194                     self::regenerateSessionId();
195                     $this->setSessionVar('UID', $this->user->getValue($this->idColumn));
196                 } else {
197                     $this->message = rex_i18n::msg('login_error');
198                 }
199             } elseif ($this->getSessionVar('UID') != '') {
200                 // wenn kein login und kein logout dann nach sessiontime checken
201                 // message schreiben und falls falsch auf error verweisen
202 
203                 $ok = true;
204 
205                 if (($this->getSessionVar('STAMP') + $this->sessionDuration) < time()) {
206                     $ok = false;
207                     $this->message = rex_i18n::msg('login_session_expired');
208 
209                     rex_csrf_token::removeAll();
210                 }
211 
212                 if ($ok && $impersonator = $this->getSessionVar('impersonator')) {
213                     $this->impersonator = rex_sql::factory($this->DB);
214                     $this->impersonator->setQuery($this->userQuery, [':id' => $impersonator]);
215 
216                     if (!$this->impersonator->getRows()) {
217                         $ok = false;
218                         $this->message = rex_i18n::msg('login_user_not_found');
219                     }
220                 }
221 
222                 if ($ok) {
223                     $query = $this->impersonator && $this->impersonateQuery ? $this->impersonateQuery : $this->userQuery;
224                     $this->user = rex_sql::factory($this->DB);
225                     $this->user->setQuery($query, [':id' => $this->getSessionVar('UID')]);
226 
227                     if (!$this->user->getRows()) {
228                         $ok = false;
229                         $this->message = rex_i18n::msg('login_user_not_found');
230                     }
231                 }
232             }
233         } else {
234             $this->message = rex_i18n::msg('login_logged_out');
235 
236             rex_csrf_token::removeAll();
237         }
238 
239         if ($ok) {
240             // wenn alles ok dann REX[UID][system_id] schreiben
241             $this->setSessionVar('STAMP', time());
242 
243             // each code-path which set $ok=true, must also set a UID
244             $sessUid = $this->getSessionVar('UID');
245             if (empty($sessUid)) {
246                 throw new rex_exception('Login considered successfull but no UID found');
247             }
248         } else {
249             // wenn nicht, dann UID loeschen und error seite
250             $this->setSessionVar('STAMP', '');
251             $this->setSessionVar('UID', '');
252             $this->setSessionVar('impersonator', null);
253         }
254 
255         if ($ok) {
256             $this->loginStatus = 1;
257         } else {
258             $this->loginStatus = -1;
259         }
260 
261         return $ok;
262     }
263 
264     public function impersonate($id)
265     {
266         if (!$this->user) {
267             throw new RuntimeException('Can not impersonate a user without valid user session.');
268         }
269         if ($this->user->getValue($this->idColumn) == $id) {
270             throw new RuntimeException('Can not impersonate the current user.');
271         }
272 
273         $user = rex_sql::factory($this->DB);
274         $user->setQuery($this->impersonateQuery ?: $this->userQuery, [':id' => $id]);
275 
276         if (!$user->getRows()) {
277             throw new RuntimeException(sprintf('User with id "%d" not found.', $id));
278         }
279 
280         $this->impersonator = $this->user;
281         $this->user = $user;
282 
283         $this->setSessionVar('UID', $id);
284         $this->setSessionVar('impersonator', $this->impersonator->getValue($this->idColumn));
285     }
286 
287     public function depersonate()
288     {
289         if (!$this->impersonator) {
290             throw new RuntimeException('There is no current impersonator.');
291         }
292 
293         $this->user = $this->impersonator;
294         $this->impersonator = null;
295 
296         $this->setSessionVar('UID', $this->user->getValue($this->idColumn));
297         $this->setSessionVar('impersonator', null);
298     }
299 
300     /**
301      * @return null|rex_sql
302      */
303     public function getUser()
304     {
305         return $this->user;
306     }
307 
308     /**
309      * @return null|rex_sql
310      */
311     public function getImpersonator()
312     {
313         return $this->impersonator;
314     }
315 
316     /**
317      * Gibt einen Benutzer-Spezifischen Wert zurück.
318      */
319     public function getValue($value, $default = null)
320     {
321         if ($this->user) {
322             return $this->user->getValue($value);
323         }
324 
325         return $default;
326     }
327 
328     /**
329      * Setzte eine Session-Variable.
330      */
331     public function setSessionVar($varname, $value)
332     {
333         $_SESSION[static::getSessionNamespace()][$this->systemId][$varname] = $value;
334     }
335 
336     /**
337      * Gibt den Wert einer Session-Variable zurück.
338      */
339     public function getSessionVar($varname, $default = '')
340     {
341         static $sessChecked = false;
342         // validate session-id - once per request - to prevent fixation
343         if (!$sessChecked) {
344             $rexSessId = !empty($_SESSION['REX_SESSID']) ? $_SESSION['REX_SESSID'] : '';
345 
346             if (!empty($rexSessId) && $rexSessId !== session_id()) {
347                 // clear redaxo related session properties on a possible attack
348                 $_SESSION[static::getSessionNamespace()][$this->systemId] = [];
349             }
350             $sessChecked = true;
351         }
352 
353         if (isset($_SESSION[static::getSessionNamespace()][$this->systemId][$varname])) {
354             return $_SESSION[static::getSessionNamespace()][$this->systemId][$varname];
355         }
356 
357         return $default;
358     }
359 
360     /*
361      * refresh session on permission elevation for security reasons
362      */
363     protected static function regenerateSessionId()
364     {
365         if ('' != session_id()) {
366             session_regenerate_id(true);
367 
368             $cookieParams = static::getCookieParams();
369             if ($cookieParams['samesite']) {
370                 self::rewriteSessionCookie($cookieParams['samesite']);
371             }
372 
373             rex_csrf_token::removeAll();
374         }
375 
376         // session-id is shared between frontend/backend or even redaxo instances per server because it's the same http session
377         $_SESSION['REX_SESSID'] = session_id();
378     }
379 
380     /**
381      * starts a http-session if not already started.
382      */
383     public static function startSession()
384     {
385         if (session_id() == '') {
386             $cookieParams = static::getCookieParams();
387 
388             session_set_cookie_params(
389                 $cookieParams['lifetime'],
390                 $cookieParams['path'],
391                 $cookieParams['domain'],
392                 $cookieParams['secure'],
393                 $cookieParams['httponly']
394             );
395 
396             $started = rex_timer::measure(__METHOD__, function () {
397                 return @session_start();
398             });
399             if (!$started) {
400                 $error = error_get_last();
401                 if ($error) {
402                     rex_error_handler::handleError($error['type'], $error['message'], $error['file'], $error['line']);
403                 } else {
404                     throw new rex_exception('Unable to start session!');
405                 }
406             }
407 
408             if ($cookieParams['samesite']) {
409                 self::rewriteSessionCookie($cookieParams['samesite']);
410             }
411         }
412     }
413 
414     /**
415      * Einstellen der Cookie Paramter bevor die session gestartet wird.
416      *
417      * @return array
418      */
419     private static function getCookieParams()
420     {
421         $cookieParams = session_get_cookie_params();
422 
423         $key = rex::isBackend() ? 'backend' : 'frontend';
424         $sessionConfig = rex::getProperty('session', []);
425 
426         if ($sessionConfig) {
427             foreach ($sessionConfig[$key]['cookie'] as $name => $value) {
428                 if ($value !== null) {
429                     $cookieParams[$name] = $value;
430                 }
431             }
432         }
433 
434         return $cookieParams;
435     }
436 
437     /**
438      * php does not natively support SameSite for cookies yet,
439      * rewrite the session cookie manually.
440      *
441      * see https://wiki.php.net/rfc/same-site-cookie
442      *
443      * @param "Strict"|"Lax" $sameSite
444      */
445     private static function rewriteSessionCookie($sameSite)
446     {
447         $cookiesHeaders = [];
448 
449         // since header_remove() will remove all sent cookies, we need to collect all of them,
450         // rewrite only the session cookie and send all cookies again.
451         $cookieHeadersPrefix = 'Set-Cookie: ';
452         $sessionCookiePrefix = 'Set-Cookie: '. session_name() .'=';
453         foreach (headers_list() as $rawHeader) {
454             // rewrite the session cookie
455             if (substr($rawHeader, 0, strlen($sessionCookiePrefix)) === $sessionCookiePrefix) {
456                 $rawHeader .= '; SameSite='. $sameSite;
457             }
458             // collect all cookies
459             if (substr($rawHeader, 0, strlen($cookieHeadersPrefix)) === $cookieHeadersPrefix) {
460                 $cookiesHeaders[] = $rawHeader;
461             }
462         }
463 
464         // remove all cookies
465         header_remove('Set-Cookie');
466 
467         // re-add all (inl. the rewritten session cookie)
468         foreach ($cookiesHeaders as $rawHeader) {
469             header($rawHeader);
470         }
471     }
472 
473     /**
474      * Verschlüsselt den übergebnen String.
475      */
476     public static function passwordHash($password, $isPreHashed = false)
477     {
478         $password = $isPreHashed ? $password : sha1($password);
479         return password_hash($password, PASSWORD_DEFAULT);
480     }
481 
482     public static function passwordVerify($password, $hash, $isPreHashed = false)
483     {
484         $password = $isPreHashed ? $password : sha1($password);
485         return password_verify($password, $hash);
486     }
487 
488     public static function passwordNeedsRehash($hash)
489     {
490         return password_needs_rehash($hash, PASSWORD_DEFAULT);
491     }
492 
493     /**
494      * returns the current session namespace.
495      *
496      * @return string
497      */
498     protected static function getSessionNamespace()
499     {
500         return rex_request::getSessionNamespace();
501     }
502 }
503