gluon-web-remote

Web remote Administration for big size of gluon routers
git clone git://archive.git.mtrnord.blog/MTRNord/gluon-web-remote.git
Log | Files | Refs | README

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 }