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
|
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Session;
use Piwik\Auth;
use Piwik\AuthResult;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\Plugins\UsersManager\Model as UsersModel;
use Piwik\Session;
use Piwik\Log\LoggerInterface;
/**
* Validates already authenticated sessions.
*
* See {@link \Piwik\Session\SessionFingerprint} for more info.
*/
class SessionAuth implements Auth
{
/**
* For tests, since there's no actual session there.
*
* @var bool
*/
private $shouldDestroySession;
/**
* @var UsersModel
*/
private $userModel;
/**
* Set internally so it can be queried in FrontController.
*
* @var array
*/
private $user;
private $tokenAuth;
/**
* @var bool
*/
private $sessionExpired = false;
public function __construct(?UsersModel $userModel = null, $shouldDestroySession = true)
{
$this->userModel = $userModel ?: new UsersModel();
$this->shouldDestroySession = $shouldDestroySession;
}
public function getName()
{
// empty
}
public function setTokenAuth(
#[\SensitiveParameter]
$token_auth
) {
$this->tokenAuth = $token_auth;
}
public function getLogin()
{
if (isset($this->user['login'])) {
return $this->user['login'];
}
}
public function getTokenAuthSecret()
{
// empty
}
public function setLogin($login)
{
// empty
}
public function setPassword(
#[\SensitiveParameter]
$password
) {
// empty
}
public function setPasswordHash(
#[\SensitiveParameter]
$passwordHash
) {
// empty
}
public function authenticate()
{
$this->sessionExpired = false;
$sessionFingerprint = new SessionFingerprint();
$userModel = $this->userModel;
$this->checkIfSessionFailedToRead();
if ($this->isExpiredSession($sessionFingerprint)) {
$sessionFingerprint->clear();
return $this->makeAuthFailure();
}
$userForSession = $sessionFingerprint->getUser();
if (empty($userForSession)) {
return $this->makeAuthFailure();
}
$user = $userModel->getUser($userForSession);
if (
empty($user)
|| $user['login'] !== $userForSession // sanity check in case there's a bug in getUser()
) {
return $this->makeAuthFailure();
}
$tsPasswordModified = !empty($user['ts_password_modified']) ? $user['ts_password_modified'] : null;
if ($this->isSessionStartedBeforePasswordChange($sessionFingerprint, $tsPasswordModified)) {
$this->destroyCurrentSession($sessionFingerprint);
return $this->makeAuthFailure();
}
$this->updateSessionExpireTime($sessionFingerprint);
if (
$this->tokenAuth !== null
&& $this->tokenAuth !== false
&& $this->tokenAuth !== $sessionFingerprint->getSessionTokenAuth()
) {
return $this->makeAuthFailure();
}
if ($sessionFingerprint->getSessionTokenAuth()) {
$tokenAuth = $sessionFingerprint->getSessionTokenAuth();
} else {
$tokenAuth = $this->userModel->generateRandomTokenAuth();
}
return $this->makeAuthSuccess($user, $tokenAuth);
}
private function isSessionStartedBeforePasswordChange(SessionFingerprint $sessionFingerprint, $tsPasswordModified)
{
// sanity check, make sure users can still login if the ts_password_modified column does not exist
if ($tsPasswordModified === null) {
return false;
}
// if the session start time doesn't exist for some reason, log the user out
$sessionStartTime = $sessionFingerprint->getSessionStartTime();
if (empty($sessionStartTime)) {
return true;
}
return $sessionStartTime < Date::factory($tsPasswordModified)->getTimestampUTC();
}
private function makeAuthFailure()
{
return new AuthResult(AuthResult::FAILURE, null, null);
}
private function makeAuthSuccess(
$user,
#[\SensitiveParameter]
$tokenAuth
) {
$this->user = $user;
$this->tokenAuth = $tokenAuth;
$isSuperUser = (int) $user['superuser_access'];
$code = $isSuperUser ? AuthResult::SUCCESS_SUPERUSER_AUTH_CODE : AuthResult::SUCCESS;
return new AuthResult($code, $user['login'], $tokenAuth);
}
protected function initNewBlankSession(SessionFingerprint $sessionFingerprint)
{
// this user should be using a different session, so generate a new ID
// NOTE: Zend_Session cannot be used since it will destroy the old
// session.
if ($this->shouldDestroySession) {
session_regenerate_id();
}
// regenerating the ID will create a new session w/ a new ID, but will
// copy over the existing session data. we want the new session for the
// unauthorized user to be different, so we clear the session fingerprint.
$sessionFingerprint->clear();
}
protected function destroyCurrentSession(SessionFingerprint $sessionFingerprint)
{
// Note: Piwik will attempt to create another session in the LoginController
// when rendering the login form (the nonce for the form is stored in the session).
// So we can't use Session::destroy() since Zend prohibits starting a new session
// after session_destroy() is called. Instead we clear the session fingerprint for
// the existing session and generate a new session. Both the old session &
// new session should have no stored data.
$sessionFingerprint->clear();
if ($this->shouldDestroySession) {
Session::regenerateId();
}
}
public function getTokenAuth()
{
return $this->tokenAuth;
}
private function updateSessionExpireTime(SessionFingerprint $sessionFingerprint)
{
$sessionParams = session_get_cookie_params();
// we update the session cookie to make sure expired session cookies are not available client side...
$sessionCookieLifetime = Config::getInstance()->General['login_cookie_expire'];
Session::writeCookie(
session_name(),
session_id(),
time() + $sessionCookieLifetime,
$sessionParams['path'],
$sessionParams['domain'],
$sessionParams['secure'],
$sessionParams['httponly'],
Session::getSameSiteCookieValue()
);
// ...and we also update the expiration time stored server side so we can prevent expired sessions from being reused
$sessionFingerprint->updateSessionExpirationTime();
}
private function isExpiredSession(SessionFingerprint $sessionFingerprint)
{
$expirationTime = $sessionFingerprint->getExpirationTime();
if (empty($expirationTime)) {
return true;
}
$isExpired = Date::now()->getTimestampUTC() > $expirationTime;
if ($isExpired) {
$this->sessionExpired = true;
}
return $isExpired;
}
public function wasSessionExpired(): bool
{
return $this->sessionExpired;
}
private function checkIfSessionFailedToRead()
{
if (Session\SaveHandler\DbTable::$wasSessionToLargeToRead) {
StaticContainer::get(LoggerInterface::class)->warning(
"Too much data stored in the session so it could not be read properly. If you were logged out, this is why."
);
}
}
}
|