Checking for "expired" sessions/logins on the client side

Getting "Expired token" errors ? Here is a solution to avoid invalid CSRF on POST or ajax requests, or user identity changes.

The YII_CSRF_TOKEN validity is a real pain actually if your end user maintains open tabs when restarting the browser. Several browsers (Opera and Safari being the most persisting) will not fetch the page again from your site, but take it from browser cache. Several Cache-Control directives are simply ignored by the browser, and even more when the protocol is http rather than https.

As a result, your web page will have an old YII_CSRF_TOKEN in it because that token is only valid as long as the session is valid - and the session ends when the browser is closed. So when the browser is started again, the YII_CSRF_TOKEN is gone or replaced by another one if you have ajax actions going on in the background.

In either case, the user may fill out a form and submit it (and see it fail), or the user might be expecting ajax updates (which may silently fail in the background).

Therefore, I developed the code below. It has undergone several iterations in order to counterbalance the behavior of the navigators in the field as 'invalid token' messages appeared in the application log.

It works like this:

  • It checks the YII_CRSF_TOKEN cookie with the expected cookie on the client.
  • It also checks a cookie containing an MD5 value calculated from the user identity.
  • When one of these cookies fail the comparisson, the page is reloaded if this was the initial page load, or, the user is prompted with a popup indicating that the page must be reloaded. In order to stop further invalid ajax requests to the server, all timers are removed before the popup appears.

It is possible to supply your own JavaScript code that should be run when the check fails (to have a different popup, etc.). There is a proposed method to generate a jQuery UI popup, and another one just using 'alert'. In the proposed methods, the popups are modal to force the user to reload or close the page.

You should use your own CWebUser subclass as indicated below for full functionnality.

I haven't set up a test case to demonstrate the issue, but the following procedure should demonstrate the issue:

  • Open a web page in your browser with a form relying on the YII_CSRF_TOKEN for submitting the data.
  • Close the browser (with the reopen tabs functionnality active);
  • Reopen the browser -> your form page should appear.
  • Try to submit the form - submission should not work (if your browser did not reload the page).
/**
     * Monitor if session expired; show jQuery dialog when it did expire.
     * 
     * Call this somewhere in the page generation process (e.g., in the layout).
     * 
     * @param int $timeout Time between checks in seconds.
     */
    public static function MonitorSessionJQueryDialog($timeout=2,$showCloseButton=true) {
        Yii::app()->clientScript->registerCoreScript('jquery.ui');
        $title=CJavaScript::encode(Yii::t('app','Session Expired'));
        $msg=CJavaScript::encode(CHtml::tag('div',array(),Yii::t('app','Your session expired and this page must be reloaded.')));
        $btReload=CJavaScript::encode(Yii::t('app','Reload'));
        if($showCloseButton) {
            $btClose=CJavaScript::encode(Yii::t('app','Close'));
            $btClose.= ":function(){_ok=true;jQuery(this).dialog('close');window.open(location.href, '_self').close(); },";
        } else {
            $btClose="";
        }
        $debug="";//"debugger;";
        $jsActionCode="var _ok=false;if(init){location.reload(true);}else{jQuery($msg).dialog({modal:true,closeOnEscape:false,title:$title,beforeclose:function(){return _ok;},buttons:{ $btClose $btReload:function(){_ok=true;jQuery(this).dialog('close');location.reload(true);}}});}$debug";
        self::MonitorSession($timeout,$jsActionCode);
    }

    /**
     * Updates the cookie used in for session monitoring status.
     *
     * @return CHttpCookie
     */
    public static function MonitorUpdateCookie() {
        $id=CHtml::value(Yii::app(),'user.id');
        $md5=md5("MonitorSession".$id);
        $name=md5(Yii::app()->id."uid");;
        if(!Yii::app()->request->cookies->contains('name')) {
            $cookie = new CHttpCookie($name, $md5);
            Yii::app()->request->cookies->add($name, $cookie);
            return $cookie;
        } else {
            $cookie = Yii::app()->request->cookies[$name];
            $cookie->value=$md5;
            return $cookie;
        }
    }
    /**
     * Monitor if session expired, do some JavaScript action when it does expire.
     *
     * Call this somewhere in the page generation process (e.g., in the layout).
     *
     * @param int $timeout Time between checks in seconds.
     * @param string $jsActionCode JavaScript action code to do; defaults to alert
     *                             popup followad by page reload.
     *                             Can use 'init' variable which is true when the 
     *                              checks fail on initial page load.
     */
    public static function MonitorSession($timeoutSeconds=2,$jsActionCode=null,$clearTimers=true) {
        $timeoutMs=$timeoutSeconds*1000;

        $cookie=self::MonitorUpdateCookie();
        if(!Yii::app()->request->isAjaxRequest) {
            Yii::app()->clientScript->registerCoreScript('cookie');
            if($jsActionCode===null) {
                $expiredMessage=CJavaScript::encode(Yii::t('app','Your session expired and this page must be reloaded.'));
                $jsActionCode=<<<EOJS
                     if(!init){alert($expiredMessage);}
                     location.reload(true);
EOJS;
            }
            /* @var string $checkCode JavaScript code that checks the conditions (result in check).*/
            $checkCode="";
            if(!Yii::app()->user->getIsGuest()) {
                $jsKey="";
                /*
                if(Yii::app()->user->allowAutoLogin) {
                    $stateCookieKey = Yii::app()->user->getStateKeyPrefix();
                    // Login is saved in cookie - otherwise session ended.
                    $jsKey="check|=($.cookie('$stateCookieKey')===null);";
                    //$jsKey="if($.cookie('$stateCookieKey')===null) console.log('No $stateCookieKey');";
                }*/
                $checkCode.=<<<EOJS
                if($.cookie) {
                    var id=$.cookie('{$cookie->name}');$jsKey
                    check|=(id===null||!id.match('{$cookie->value}'));
                }
EOJS;
            }
            if(Yii::app()->request->enableCsrfValidation) {
                $csrfTokenName = CJavaScript::encode(Yii::app()->request->csrfTokenName);
                $csrfTokenValueRegex= CJavaScript::encode('"'.Yii::app()->request->csrfToken.'"');

                $checkCode.=<<<EOJS
                if($.cookie) {
                    var csrf=$.cookie($csrfTokenName);
                    check|=(csrf===null||!csrf.match($csrfTokenValueRegex));
                }
EOJS;
            }
            $jsClearTimers='for(var i=setTimeout(function(){}, 0); i >=0; i-=1) {clearTimeout(i);}';
            if($clearTimers) {
                $jsActionCode=$jsClearTimers.$jsActionCode;
            }
            if($checkCode!=="") {
                $jsMonitorScript=<<<EOJS
                    (function($) {
                        "strict";
                        var timer;
                        function checkSession(init) {
                            var check=false;
                            $checkCode
                            if(check) {window.clearInterval(timer);$jsActionCode}
                        }
                        function startCheck(){
                            timer=window.setInterval(checkSession,$timeoutMs);
                            checkSession(true);
                        }
                        window.onbeforeunload=function(e){
                            window.clearInterval(timer);
                        }
                        timer=window.setTimeout(startCheck,100);
                    })(jQuery);
EOJS;
                Yii::app()->clientScript->registerScript(__FILE__."#MonitorSession", $jsMonitorScript,CClientScript::POS_READY);
            }
        }
    }
}

To make this also work when you do not use a cookie for login validation, you should also modify your WebUser class to make sure that there is at least one cookie available for monitoring.

public function loginRequired() {
        /* Make sure that monitoring cookie is "up-to-date" is login is required */
        Utils::MonitorUpdateCookie();
        parent::loginRequired();
    }
    public function afterLogin($fromCookie) {
        /* Make sure that the monitoring cookie is set to the current user after login */
        Utils::MonitorUpdateCookie();
    }

    public function afterLogout() {
        /* Make sure that the monitoring cookie is set to "no user" after logout */
        Utils::MonitorUpdateCookie();
    }

P.S.: Some of the above code is commented - this is a copy/paste of my code and the commented code is there for debug or for reference to code that I might need to add again in some form or the other.

Forum

If you have questions, go to the forum page.