OmarElGabry/miniPHP

View on GitHub
app/core/Session.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/**
 * Session Class
 *
 * @license    http://opensource.org/licenses/MIT The MIT License (MIT)
 * @author     Omar El Gabry <omar.elgabry.93@gmail.com>
 */

class Session{

    /**
     * constructor for Session Object.
     *
     * @access private
     */
    private function __construct() {}

    /**
     * Starts the session if not started yet.
     *
     * @access public
     *
     */
    public static function init(){

        if (session_status() == PHP_SESSION_NONE) {     // if (session_id() == '')
            session_start();
        }
    }

    /**
     * Checks if session data exists and valid or not.
     *
     * @access public
     * @static static method
     * @param  string $ip
     * @param  string $userAgent
     * @return boolean
     *
     */
    public static function isSessionValid($ip, $userAgent){

        $isLoggedIn  = self::getIsLoggedIn();
        $userId      = self::getUserId();
        $userRole    = self::getUserRole();

        // 1. check if there is any data in session
        if(empty($isLoggedIn) || empty($userId) || empty($userRole)){
            return false;
        }

        /*if(!self::isConcurrentSessionExists()){
            self::remove();
            return false;
        }*/

        // 2. then check ip address and user agent
        if(!self::validateIPAddress($ip) || !self::validateUserAgent($userAgent)) {
            Logger::log("SESSION", "current session is invalid", __FILE__, __LINE__);
            self::remove();
            return false;
        }

        // 3. check if session is expired
        if(!self::validateSessionExpiry()){
            self::remove();
            return false;
        }

        return true;
    }

    /**
     * Get IsLoggedIn value(boolean)
     *
     * @access public
     * @static static method
     * @return boolean
     *
     */
    public static function getIsLoggedIn(){
        return empty($_SESSION["is_logged_in"]) || !is_bool($_SESSION["is_logged_in"]) ? false : $_SESSION["is_logged_in"];
    }

    /**
     * Get User ID.
     *
     * @access public
     * @static static method
     * @return string|null
     *
     */
    public static function getUserId(){
        return empty($_SESSION["user_id"]) ? null : (int)$_SESSION["user_id"];
    }

    /**
     * Get User Role
     *
     * @access public
     * @static static method
     * @return string|null
     *
     */
    public static function getUserRole(){
        return empty($_SESSION["role"]) ? null : $_SESSION["role"];
    }

    /**
     * Get CSRF Token
     *
     * @access public
     * @static static method
     * @return string|null
     *
     */
    public static function getCsrfToken(){
        return empty($_SESSION["csrf_token"]) ? null : $_SESSION["csrf_token"];
    }

    /**
     * Get CSRF Token generated time
     *
     * @access public
     * @static static method
     * @return string|null
     *
     */
    public static function getCsrfTokenTime(){
        return empty($_SESSION["csrf_token_time"]) ? null : $_SESSION["csrf_token_time"];
    }

    /**
     * set session key and value
     *
     * @access public
     * @static static method
     * @param $key
     * @param $value
     *
     */
    public static function set($key, $value){
        $_SESSION[$key] = $value;
    }

    /**
     * get session value by $key
     *
     * @access public
     * @static static method
     * @param  $key
     * @return mixed
     *
     */
    public static function get($key){
        return array_key_exists($key, $_SESSION)? $_SESSION[$key]: null;
    }

    /**
     * get session value by $key and destroy it
     *
     * @access public
     * @static static method
     * @param  $key
     * @return mixed
     *
     */
    public static function getAndDestroy($key){

        if(array_key_exists($key, $_SESSION)){

            $value = $_SESSION[$key];
            $_SESSION[$key] = null;
            unset($_SESSION[$key]);

            return $value;
        }
        
        return null;
    }

    /**
     * matches current IP Address with the one stored in the session
     *
     * @access public
     * @static static method
     * @param  string $ip
     * @return bool
     *
     */
    private static function validateIPAddress($ip){

        if(!isset($_SESSION['ip']) || !isset($ip)) {
            return false;
        }

        return $_SESSION['ip'] === $ip;
    }

    /**
     * matches current user agent with the one stored in the session
     *
     * @access public
     * @static static method
     * @param  string $userAgent
     * @return bool
     *
     */
    private static function validateUserAgent($userAgent){

        if(!isset($_SESSION['user_agent']) || !isset($userAgent)) {
            return false;
        }

        return $_SESSION['user_agent'] === $userAgent;
    }

    /**
     * checks if session has been expired
     *
     * @access public
     * @static static method
     * @return bool
     *
     */
    private static function validateSessionExpiry(){

        $max_time = 60 * 60 * 24; // 1 day

        if(!isset($_SESSION['generated_time'])) {
            return false;
        }

        return ($_SESSION['generated_time'] + $max_time) > time();
    }

    /**
     * checks for session concurrency
     *
     * This is done as the following:
     * UserA logs in with his session id('123') and it will be stored in the database.
     * Then, UserB logs in also using the same email and password of UserA from another PC,
     * and also store the session id('456') in the database
     *
     * Now, Whenever UserA performs any action,
     * You then check the session_id() against the last one stored in the database('456'),
     * If they don't match then log both of them out.
     *
     * @access public
     * @static static method
     * @return bool
     * @see Session::updateSessionId()
     * @see http://stackoverflow.com/questions/6126285/php-stop-concurrent-user-logins
     */
    public static function isConcurrentSessionExists(){

        $session_id = session_id();
        $userId  = self::getUserId();

        if(isset($userId) && isset($session_id)){

            $database = Database::openConnection();
            $database->prepare("SELECT session_id FROM users WHERE id = :id LIMIT 1");

            $database->bindValue(":id", $userId);
            $database->execute();
            $result = $database->fetchAssociative();
            $userSessionId = !empty($result)? $result['session_id']: null;

            return $session_id !== $userSessionId;
        }

        return false;
    }

    /**
     * get CSRF token and generate a new one if expired
     *
     * @access public
     * @static static method
     * @return string
     *
     */
    public static function generateCsrfToken(){

        $max_time = 60 * 60 * 24; // 1 day
        $stored_time = self::getCsrfTokenTime();
        $csrf_token  = self::getCsrfToken();

        if($max_time + $stored_time <= time() || empty($csrf_token)){
            $token = md5(uniqid(rand(), true));
            $_SESSION["csrf_token"] = $token;
            $_SESSION["csrf_token_time"] = time();
        }

        return self::getCsrfToken();
    }

    /**
     * reset session id, delete session file on server, and re-assign the values.
     *
     * @access public
     * @static static method
     * @param  array  $data
     * @return string
     *
     */
    public static function reset($data){

        // remove old and regenerate session ID.
        session_regenerate_id(true);
        $_SESSION = array();

        $_SESSION["is_logged_in"] = true;
        $_SESSION["user_id"]      = (int)$data["user_id"];
        $_SESSION["role"]         = $data["role"];

        // save these values in the session,
        // they are needed to avoid session hijacking and fixation
        $_SESSION['ip']             = $data["ip"];
        $_SESSION['user_agent']     = $data["user_agent"];
        $_SESSION['generated_time'] = time();

        // update session id in database
        self::updateSessionId($data["user_id"], session_id());

        // set session cookie setting manually,
        // Why? because you need to explicitly set session expiry, path, domain, secure, and HTTP.
        // @see https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet#Cookies
        setcookie(session_name(), session_id(), time() + Config::get('SESSION_COOKIE_EXPIRY') /*a week*/, Config::get('COOKIE_PATH'), Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
    }

    /**
     * update session id in database
     *
     * @access public
     * @static static method
     * @param  string $userId
     * @param  string $sessionId
     * @return string
     *
     */
    private static function updateSessionId($userId, $sessionId = null){

        $database = Database::openConnection();
        $database->prepare("UPDATE users SET session_id = :session_id WHERE id = :id");

        $database->bindValue(":session_id", $sessionId);
        $database->bindValue(":id", $userId);
        $database->execute();
    }

    /**
     * Remove the session
     * Delete session completely from the browser cookies and destroy it's file on the server
     *
     * @access public
     * @static static method
     */
    public static function remove(){

        // update session in database
        $userId = self::getUserId();
        if(!empty($userId)){
            self::updateSessionId(self::getUserId());
        }

        // clear session data
        $_SESSION = array();

        // remove session cookie
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $params["secure"], $params["httponly"]
            );
        }

        // destroy session file on server(if not already)
        if(session_status() === PHP_SESSION_ACTIVE){
            session_destroy();
        }
    }

}