LoginModel.php (15640B)
1 <?php 2 3 /** 4 * LoginModel 5 * 6 * The login part of the model: Handles the login / logout stuff 7 */ 8 class LoginModel 9 { 10 /** 11 * Login process (for DEFAULT user accounts). 12 * 13 * @param $user_name string The user's name 14 * @param $user_password string The user's password 15 * @param $set_remember_me_cookie mixed Marker for usage of remember-me cookie feature 16 * 17 * @return bool success state 18 */ 19 public static function login($user_name, $user_password, $set_remember_me_cookie = null) 20 { 21 // we do negative-first checks here, for simplicity empty username and empty password in one line 22 if (empty($user_name) OR empty($user_password)) { 23 Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY')); 24 return false; 25 } 26 27 // checks if user exists, if login is not blocked (due to failed logins) and if password fits the hash 28 $result = self::validateAndGetUser($user_name, $user_password); 29 30 // check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details. 31 if (!$result) { 32 //No Need to give feedback here since whole validateAndGetUser controls gives a feedback 33 return false; 34 } 35 36 // stop the user's login if account has been soft deleted 37 if ($result->user_deleted == 1) { 38 Session::add('feedback_negative', Text::get('FEEDBACK_DELETED')); 39 return false; 40 } 41 42 // stop the user from logging in if user has a suspension, display how long they have left in the feedback. 43 if ($result->user_suspension_timestamp != null && $result->user_suspension_timestamp - time() > 0) { 44 $suspensionTimer = Text::get('FEEDBACK_ACCOUNT_SUSPENDED') . round(abs($result->user_suspension_timestamp - time())/60/60, 2) . " hours left"; 45 Session::add('feedback_negative', $suspensionTimer); 46 return false; 47 } 48 49 // reset the failed login counter for that user (if necessary) 50 if ($result->user_last_failed_login > 0) { 51 self::resetFailedLoginCounterOfUser($result->user_name); 52 } 53 54 // save timestamp of this login in the database line of that user 55 self::saveTimestampOfLoginOfUser($result->user_name); 56 57 // if user has checked the "remember me" checkbox, then write token into database and into cookie 58 if ($set_remember_me_cookie) { 59 self::setRememberMeInDatabaseAndCookie($result->user_id); 60 } 61 62 // successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true 63 self::setSuccessfulLoginIntoSession( 64 $result->user_id, $result->user_name, $result->user_email, $result->user_account_type 65 ); 66 67 // return true to make clear the login was successful 68 // maybe do this in dependence of setSuccessfulLoginIntoSession ? 69 return true; 70 } 71 72 /** 73 * Validates the inputs of the users, checks if password is correct etc. 74 * If successful, user is returned 75 * 76 * @param $user_name 77 * @param $user_password 78 * 79 * @return bool|mixed 80 */ 81 private static function validateAndGetUser($user_name, $user_password) 82 { 83 // brute force attack mitigation: use session failed login count and last failed login for not found users. 84 // block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago 85 // (limits user searches in database) 86 if (Session::get('failed-login-count') >= 3 AND (Session::get('last-failed-login') > (time() - 30))) { 87 Session::add('feedback_negative', Text::get('FEEDBACK_LOGIN_FAILED_3_TIMES')); 88 return false; 89 } 90 91 // get all data of that user (to later check if password and password_hash fit) 92 $result = UserModel::getUserDataByUsername($user_name); 93 94 // check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details. 95 // brute force attack mitigation: reset failed login counter because of found user 96 if (!$result){ 97 // increment the user not found count, helps mitigate user enumeration 98 self::incrementUserNotFoundCounter(); 99 // user does not exist, but we won't to give a potential attacker this details, so we just use a basic feedback message 100 Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG')); 101 return false; 102 } 103 104 // block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago 105 if (($result->user_failed_logins >= 3) AND ($result->user_last_failed_login > (time() - 30))) { 106 Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_WRONG_3_TIMES')); 107 return false; 108 } 109 110 // if hash of provided password does NOT match the hash in the database: +1 failed-login counter 111 if (!password_verify($user_password, $result->user_password_hash)) { 112 self::incrementFailedLoginCounterOfUser($result->user_name); 113 Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG')); 114 return false; 115 } 116 117 // if user is not active (= has not verified account by verification mail) 118 if ($result->user_active != 1) { 119 Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET')); 120 return false; 121 } 122 123 // reset the user not found counter 124 self::resetUserNotFoundCounter(); 125 126 return $result; 127 } 128 129 /** 130 * Reset the failed-login-count to 0. 131 * Reset the last-failed-login to an empty string. 132 */ 133 private static function resetUserNotFoundCounter() 134 { 135 Session::set('failed-login-count', 0); 136 Session::set('last-failed-login', ''); 137 } 138 139 /** 140 * Increment the failed-login-count by 1. 141 * Add timestamp to last-failed-login. 142 */ 143 private static function incrementUserNotFoundCounter() 144 { 145 // Username enumeration prevention: set session failed login count and last failed login for users not found 146 Session::set('failed-login-count', Session::get('failed-login-count') + 1); 147 Session::set('last-failed-login', time()); 148 } 149 150 /** 151 * performs the login via cookie (for DEFAULT user account, FACEBOOK-accounts are handled differently) 152 * TODO add throttling here ? 153 * 154 * @param $cookie string The cookie "remember_me" 155 * 156 * @return bool success state 157 */ 158 public static function loginWithCookie($cookie) 159 { 160 // do we have a cookie ? 161 if (!$cookie) { 162 Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID')); 163 return false; 164 } 165 166 // before list(), check it can be split into 3 strings. 167 if(count (explode(':', $cookie)) !== 3){ 168 Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID')); 169 return false; 170 } 171 172 // check cookie's contents, check if cookie contents belong together or token is empty 173 list ($user_id, $token, $hash) = explode(':', $cookie); 174 175 // decrypt user id 176 $user_id = Encryption::decrypt($user_id); 177 178 if ($hash !== hash('sha256', $user_id . ':' . $token) OR empty($token) OR empty($user_id)) { 179 Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID')); 180 return false; 181 } 182 183 // get data of user that has this id and this token 184 $result = UserModel::getUserDataByUserIdAndToken($user_id, $token); 185 186 // if user with that id and exactly that cookie token exists in database 187 if ($result) { 188 // successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true 189 self::setSuccessfulLoginIntoSession($result->user_id, $result->user_name, $result->user_email, $result->user_account_type); 190 // save timestamp of this login in the database line of that user 191 self::saveTimestampOfLoginOfUser($result->user_name); 192 193 // NOTE: we don't set another remember_me-cookie here as the current cookie should always 194 // be invalid after a certain amount of time, so the user has to login with username/password 195 // again from time to time. This is good and safe ! ;) 196 197 Session::add('feedback_positive', Text::get('FEEDBACK_COOKIE_LOGIN_SUCCESSFUL')); 198 return true; 199 } else { 200 Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID')); 201 return false; 202 } 203 } 204 205 /** 206 * Log out process: delete cookie, delete session 207 */ 208 public static function logout() 209 { 210 $user_id = Session::get('user_id'); 211 212 self::deleteCookie($user_id); 213 214 Session::destroy(); 215 Session::updateSessionId($user_id); 216 } 217 218 /** 219 * The real login process: The user's data is written into the session. 220 * Cheesy name, maybe rename. Also maybe refactoring this, using an array. 221 * 222 * @param $user_id 223 * @param $user_name 224 * @param $user_email 225 * @param $user_account_type 226 */ 227 public static function setSuccessfulLoginIntoSession($user_id, $user_name, $user_email, $user_account_type) 228 { 229 Session::init(); 230 231 // remove old and regenerate session ID. 232 // It's important to regenerate session on sensitive actions, 233 // and to avoid fixated session. 234 // e.g. when a user logs in 235 session_regenerate_id(true); 236 $_SESSION = array(); 237 238 Session::set('user_id', $user_id); 239 Session::set('user_name', $user_name); 240 Session::set('user_email', $user_email); 241 Session::set('user_account_type', $user_account_type); 242 Session::set('user_provider_type', 'DEFAULT'); 243 244 // get and set avatars 245 Session::set('user_avatar_file', AvatarModel::getPublicUserAvatarFilePathByUserId($user_id)); 246 Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($user_email)); 247 248 // finally, set user as logged-in 249 Session::set('user_logged_in', true); 250 251 // update session id in database 252 Session::updateSessionId($user_id, session_id()); 253 254 // set session cookie setting manually, 255 // Why? because you need to explicitly set session expiry, path, domain, secure, and HTTP. 256 // @see https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet#Cookies 257 setcookie(session_name(), session_id(), time() + Config::get('SESSION_RUNTIME'), Config::get('COOKIE_PATH'), 258 Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP')); 259 260 } 261 262 /** 263 * Increments the failed-login counter of a user 264 * 265 * @param $user_name 266 */ 267 public static function incrementFailedLoginCounterOfUser($user_name) 268 { 269 $database = DatabaseFactory::getFactory()->getConnection(); 270 271 $sql = "UPDATE users 272 SET user_failed_logins = user_failed_logins+1, user_last_failed_login = :user_last_failed_login 273 WHERE user_name = :user_name OR user_email = :user_name 274 LIMIT 1"; 275 $sth = $database->prepare($sql); 276 $sth->execute(array(':user_name' => $user_name, ':user_last_failed_login' => time() )); 277 } 278 279 /** 280 * Resets the failed-login counter of a user back to 0 281 * 282 * @param $user_name 283 */ 284 public static function resetFailedLoginCounterOfUser($user_name) 285 { 286 $database = DatabaseFactory::getFactory()->getConnection(); 287 288 $sql = "UPDATE users 289 SET user_failed_logins = 0, user_last_failed_login = NULL 290 WHERE user_name = :user_name AND user_failed_logins != 0 291 LIMIT 1"; 292 $sth = $database->prepare($sql); 293 $sth->execute(array(':user_name' => $user_name)); 294 } 295 296 /** 297 * Write timestamp of this login into database (we only write a "real" login via login form into the database, 298 * not the session-login on every page request 299 * 300 * @param $user_name 301 */ 302 public static function saveTimestampOfLoginOfUser($user_name) 303 { 304 $database = DatabaseFactory::getFactory()->getConnection(); 305 306 $sql = "UPDATE users SET user_last_login_timestamp = :user_last_login_timestamp 307 WHERE user_name = :user_name LIMIT 1"; 308 $sth = $database->prepare($sql); 309 $sth->execute(array(':user_name' => $user_name, ':user_last_login_timestamp' => time())); 310 } 311 312 /** 313 * Write remember-me token into database and into cookie 314 * Maybe splitting this into database and cookie part ? 315 * 316 * @param $user_id 317 */ 318 public static function setRememberMeInDatabaseAndCookie($user_id) 319 { 320 $database = DatabaseFactory::getFactory()->getConnection(); 321 322 // generate 64 char random string 323 $random_token_string = hash('sha256', mt_rand()); 324 325 // write that token into database 326 $sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1"; 327 $sth = $database->prepare($sql); 328 $sth->execute(array(':user_remember_me_token' => $random_token_string, ':user_id' => $user_id)); 329 330 // generate cookie string that consists of user id, random string and combined hash of both 331 // never expose the original user id, instead, encrypt it. 332 $cookie_string_first_part = Encryption::encrypt($user_id) . ':' . $random_token_string; 333 $cookie_string_hash = hash('sha256', $user_id . ':' . $random_token_string); 334 $cookie_string = $cookie_string_first_part . ':' . $cookie_string_hash; 335 336 // set cookie, and make it available only for the domain created on (to avoid XSS attacks, where the 337 // attacker could steal your remember-me cookie string and would login itself). 338 // If you are using HTTPS, then you should set the "secure" flag (the second one from right) to true, too. 339 // @see http://www.php.net/manual/en/function.setcookie.php 340 setcookie('remember_me', $cookie_string, time() + Config::get('COOKIE_RUNTIME'), Config::get('COOKIE_PATH'), 341 Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP')); 342 } 343 344 /** 345 * Deletes the cookie 346 * It's necessary to split deleteCookie() and logout() as cookies are deleted without logging out too! 347 * Sets the remember-me-cookie to ten years ago (3600sec * 24 hours * 365 days * 10). 348 * that's obviously the best practice to kill a cookie @see http://stackoverflow.com/a/686166/1114320 349 * 350 * @param string $user_id 351 */ 352 public static function deleteCookie($user_id = null) 353 { 354 // is $user_id was set, then clear remember_me token in database 355 if(isset($user_id)){ 356 357 $database = DatabaseFactory::getFactory()->getConnection(); 358 359 $sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1"; 360 $sth = $database->prepare($sql); 361 $sth->execute(array(':user_remember_me_token' => NULL, ':user_id' => $user_id)); 362 } 363 364 // delete remember_me cookie in browser 365 setcookie('remember_me', false, time() - (3600 * 24 * 3650), Config::get('COOKIE_PATH'), 366 Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP')); 367 } 368 369 /** 370 * Returns the current state of the user's login 371 * 372 * @return bool user's login status 373 */ 374 public static function isUserLoggedIn() 375 { 376 return Session::userIsLoggedIn(); 377 } 378 }