HOW TO: Use token based authentication

After reading the other wiki article regarding token based access, I thought that it might be interesting to share my method.

I have extended CWebUser (which I call YWebUser) for token based access.

In my particular implementation, the token is an encrypted value which is related to a context. Each context has its own encryption key. The context index is shown in clear, and the token is encrypted information. The decrypted information is in JSON format. That information has at least:

  • t - time information which allows limitation of the token in time;
  • id - The user id

The way I make it work is:

  • The client application gets a token from the server based on login credentials. This can be a third party server.
  • The token provided by the server is encrypted JSON data with time information (t) and user id information (id). The time field (t) ensures that the token changes over time and that the web services can check its age.
  • The advantage over the database approach is that no database is required (so no specific token management) and that a third party can generate the token (shared key).

The code below references "Context::getJSONFromEncryptedTokenToArray()". Context is my CActiveRecord model for the context, and "getJSONFromEncryptedTokenToArray()" gets the context id and token from the HTTP_REQUEST (using getParam) directly. You can replace this method with any method you like for token validation (so you could still get the token from a database).

If the token is not provided, regular cookie checking takes place.

This method also allows signing in to the application (front office) using a token encrypted by a third party.

UserIdentity::getRemote($data['context'],$remote_user_id);

in the code below converts the "remote_id" provided in the token to the local id. You may not need this conversion. In my application, I allow serveral online shops to log in to my platform (including new users). So you might have a user_id 10 in all online shops which in fact corresponds to a different user each time. So in the Yii application a new user id is created and a database table makes the link between (context_id,remote_user_id) and (user_id). The 'getRemote' method gets the local user_id (and creates it if needed).

The timestamp is also checked.

The code below has been copy/pasted from my app, but does require you to make some modifications depending on your platform (getRemote and getJSONFromEncryptedTokenToArray).

class YWebUser extends CWebUser {

    public function init() {
        $token=Yii::app()->request->getParam('token',null);
        $context_id=Yii::app()->request->getParam('c',null);

        if($token===null||$context_id===null) {
            // No token or no context: use regular method:
            //  -> Parent checks cookie.
            parent::init();
        } else {
            // Check if token is valid.
            $result=$this->checkLoginToken($context_id,$token);
            if($result!==null) {
                echo CJSON::encode($result);
                exit;
            }
        }
    }

    const ERR_INVALID_TOKEN = -1;
    const ERR_BAD_TOKEN = -2;
    const ERR_EXPIRED_TOKEN = -3;
    const ERR_INVALID_USER = -4;

    const TOKEN_EXPIRE_SECONDS = 86400;

    private function checkLoginToken() {
        $result=array();
        $data=Context::getJSONFromEncryptedTokenToArray();
        if(!isset($data['context'])||($data['context']===null)) {
            $result['error']=self::ERR_INVALID_TOKEN;
            if(Yii::app()->request->getParam('token',null)==='(null)') {
                // Reply to iPhone version with text error in this case.
                $result['error']="ERR_INVALID_TOKEN";
            }
            $result['errmsg']=Yii::t('app','Invalid token');
        } else {
            if(!(isset($data['id']) && isset($data['t']))) {
                $result['error']=self::ERR_BAD_TOKEN;
                $result['errmsg']=Yii::t('app','Bad token');
            } else {
                $remote_user_id=intval($data['id']);
                $timestamp=intval($data['t']);
                $user=UserIdentity::getRemote($data['context'],$remote_user_id);
                if($user===null) {
                    $result['error']=self::ERR_INVALID_USER;
                    $result['errmsg']=Yii::t('app','Invalid user');
                } else {
                    if($timestamp<time()-self::TOKEN_EXPIRE_SECONDS) {
                        $result['error']=self::ERR_EXPIRED_TOKEN;
                        $result['errmsg']=Yii::t('app','Expired token');
                    } else {
                        // Timestamp and user are ok.
                        $this->login($user,0); // Only login for the current request.
                        $result=null;
                    }
                }
            }
        }
        return $result;
    }
}

as the class is overloaded, you have to update your Yii configuration in a way similar to this:

// application components
        'components'=>array(
                'user'=>array(
                        'class'=>'YWebUser',
                        /* enable cookie-based authentication */
                        'allowAutoLogin'=>true,
                        'loginUrl' => array('access/login'),
                        //'loginRequiredAjaxResponse'=>'{"error":403,"message":"User not authenticated"}',
                ),
         [...]