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).
~~~
[php]
/**
* 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');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.
~~~
[php]
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](http://www.yiiframework.com/forum/index.php/topic/43781-csrf-token-invalid-for-long-sessions-and-multiple-tabs/page__p__207724__hl__expired+token#entry207724
"forum page").