Yii Soap with Authentication

You use the WebService for Yii and want users to authenticate before they use other commands..

Prerequisits

You should have set up some basic authentication for your website already and just want to enhance it with a webservice. Authentication works similar to this.

Implementation

Our goal is to have a soapclient which works like this:

$client = new SoapClient('http://127.0.0.1/yii/test/api/soap/wsdl');
// login using username and password
$session = $client->login('testuser@example.com', 'secretPassword');
// calling some other soap functions using our retrieved session key
$client->furtherMethod($session, 123);
$client->furtherMethod2($session, 'abc');
$client->furtherMethod3($session);

So you call the login command, get a sessionkey and all further requests use this sessionkey.

Actual Implementation: What the controller should look like:

Yii::import('user.components.UserIdentity');
class SoapController extends Controller
{
    /**
     * @param string the username
     * @param string the password
     * @return string a sessionkey
     * @soap
     */
    public function login($name, $password)
    {
        $identity = new UserIdentity($name, $password);
        $identity->authenticate();
        if ($identity->errorCode == UserIdentity::ERROR_NONE)
            Yii::app()->user->login($identity, 3600);
        else
            throw new SoapFault("login", "Problem with login");
        $sessionKey = sha1(mt_rand());
        Yii::app()->cache->set('soap_sessionkey'.$sessionKey.Yii::app()->user->id, $name.':'.$password, 1800);
        return $sessionKey;
    }

    /**
     * authenticates a user via the sessionid
     * throws an exception on error
     */
    protected function authenticateBySession($sessionKey)
    {
        $data = Yii::app()->cache->get('soap_sessionkey'.$sessionKey.Yii::app()->user->id);

        if (empty($data)){
           throw new SoapFault('authentication', 'Your session is invalid');
        }
        list($name, $password) = explode(':', $data);
        if ($name)
        {
            $identity = new UserIdentity($name, $password);
            $identity->authenticate();
            if ($identity->errorCode == UserIdentity::ERROR_NONE)
                Yii::app()->user->login($identity, 3600);
        }
        // happens when session is invalid or login not possible (deleted, deactivated)
        if (!Yii::app()->user->id)
            throw new SoapFault('authentication', 'Your session is invalid');
    }

    /**
     * @param string the session key
     * @param int random stuff
     * @return int current user id
     * @soap
     */
    public function furtherMethod($session, $bla)
    {
        $this->authenticateBySession($session);
        return Yii:.app()->user->id;
    }
}

This is the basic idea behind this. But I wouldn't recommend exactly this way because we store the plaintext password inside the cache.

Security improvement

My solution for this was to only store the username and don't validate the password on further requests.

protected function authenticateBySession($sessionKey)
{
    $name = Yii::app()->cache->get('soap_sessionkey'.$sessionKey.Yii::app()->user->id);

    if (empty($name)){
           throw new SoapFault('authentication', 'Your session is invalid');
    }
    else
    {
        $identity = new UserIdentity($name, '');
        $identity->authenticate(true);
        ...

Inside the UserIdentity model:

public function authenticate($passwordless = false)
{
    $user=User::model()->find('LOWER(email)=?',array(strtolower($this->username)));
    if($user && !$passwordless && !$user->validatePassword($this->password))
        // throw password is wrong error
        ...

I kept it a bit shorter because your UserIdentity model surely looks different than mine and everyone elses.. I hope you got the application working. If not you can write me an email and I try to help.

TODO

I don't know yet how to use https for webservices.

Updates

21.10.12: as Tjeerd R. pointed out one should check after cache->get if the data is empty or not (cache invalidation) which caused otherwise an hard to trace bug - Thank you :)

Commentaires: