Detecting not authorised when using AJAX

The problem that I have is detecting not authorised (i.e. not logged in) when using AJAX.

Say, I have a controller with:




public function filters() { return array( 'accessControl'); }

public function accessRules() {

    return array(

        array('allow', 'actions' => array('list', 'show', 'captcha'), 'users'=>array('*')),

        array('allow', 'users'   => array('@')),

        array('deny',  'users'   => array('*')),

    );

}


public function actionTest() { FirePHP::getInstance(true)->fb('test action entered'); }

public function actionNiceOne() { FirePHP::getInstance(true)->fb('nice one entered'); }



and a view with:




[<?php echo CHtml::ajaxLink('Nice one!', array('niceone'),array(

        'type'=>'POST',

        'data'=>'id='. $post->id,

        'dataType'=>'text',

        'beforeSend'=>'function(){ pThis.append("<br /><span class=\'gridLinkAjaxing\'>Approving</span>"); }',

        'success'=>'function(msg){ pThis.replaceWith("<span class=\'gridLinkAjaxed\'>Approved</span>"); }',

        'error'=>'function(){ pThis.replaceWith("<span class=\'gridLinkAjaxFailed\'>Failed Approving!</span>"); }'),

        array('onclick'=>'var pThis=$(this);')); ?>]

[<?php echo CHtml::link('Test Post', array('test')); ?>]



If I leave user() alone and click the non-AJAX link then I get a redirect to ‘site/login’. This works for the non-AJAX link, but the AJAX link returns a 302, which is not an error condition! Thus, feedback to the AJAX call is successful even though it has failed!

If I set ‘user’ => array(‘loginUrl’=>null), then the AJAX call errors, as required, but the non-AJAX link displays the 403 page (Unauthorized Login Required You do not have the proper credential to access this page. If you think this is a server error, please contact the webmaster. )

What I want is to allow the non-AJAX links to honour ‘user’ => array(‘loginUrl’=>‘site/login’), but for the AJAX calls to be returned an error (e.g. a 403).

With ‘user’ => array(‘loginUrl’=>null) the AJAX call returns a success even though it isn’t actioned – i.e. Post::actionNiceOne is never entered.

How do I make the AJAX call fail?

You may try this in your app config:

‘user’=>$_SERVER[‘HTTP_X_REQUESTED_WITH’]===‘XMLHttpRequest’ ? array(‘loginUrl’=>null) : array(‘loginUrl’=>‘site/login’),

Great idea. Thanks.

Just a note for anyone looking at this that the above needs an array around ‘site/login’, so:


array('loginUrl' => array('site/login')

So, in total:




'user' => $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest' ?

            array('loginUrl' => null) : array('loginUrl' => array('site/login')),



To make it even better for ajax calls you could redirect to a page that contains some javascript that then loads the login page (by doing location.href = …).




'user' => $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest' ? 

    array('loginUrl' => array('site/js_redirect_to_login')) : 

    array('loginUrl' => array('site/login')),



(updated)

To get a really clean solution for this problem is not easy, I have not seen many web applications that handle this in a nice way. The main problem is that AJAX calls usually have a target in the DOM, so when the user got logged out or an error occured then the unexpected server answer would be displayed in some section of your web page. One solution is to also transmit status information in a json header. All this has to be evaluated in some callback functions in the client’s javascript which gets much more complicated because of this.

Yes, that’s cool. So many ways!

Note that you have transcribed the previous problem by mistake. Hence, you meant:




    array('loginUrl' => array('site/js_redirect_to_login')) : 



Good point. I think that the yii helper ajax functions wrap jquery quite well to allow handling this okay. With a little extra jquery knowledge, you can do magic. Anonymous functions and events are fun. But, as you say, managing a genic, cross-site solution needs thought.