Zend_Auth: Allow user to be logged in to multiple tables/identities
Asked Answered
F

4

9

I am using Zend_Auth for authentication in a web portal.

A normal mySQL "users" table with a login and password column gets queried against, and a user logged in.

However, I have two additional groups of users that I want to authenticate. All three of these user groups have their logon data in other tables. Their data is coming from external sources, so unifying these login accounts into one is not desired.

So it could be that a user is an authenticated user from any of the three groups, even all three of them, at the same time.

Every one of the three login groups has their own login form and logout button.

At the moment, I have a single, straightforward Zend_Auth login, taken from some tutorial and slightly modified, that looks approximately like this:

function login($user, $password)
{

$auth = Zend_Auth::getInstance();
$storage = new Zend_Auth_Storage_Session();

$auth->setStorage($storage);

$adapter = new Zend_Auth_Adapter_DbTable(....);

$adapter->setIdentity($username)->setCredential($password); 

$result = $auth->authenticate($adapter);

if ($result->isValid())
 ......... success!
else 
 .... fail!

where would I have to start to make this serve and address separate "logged in" states for the three groups? My idea is that I would like to share the session, and manage the authentications separately.

Is this possible? Maybe there is a simple prefix that makes this easy? Do any tutorials or resources exist on the issue?

I'm a relative newbie to the Zend Framework.

Foresheet answered 2/8, 2010 at 16:20 Comment(15)
Do you need to have separate identities for each group? Or is just a matter of authenticating against a different table?Dandle
@Dandle they would have to be separate identities (say, CMS users, wholesalers, and end users). I could be all three things at the same time.Foresheet
Prior ZF this was the domain of PEAR::LiveUser. devzone.zend.com/node/view/id/1001Sula
@leppie: Please refrain from using abusive and offensive language in the edit revision comments or anywhere else on Stack Overflow.Brei
Are you wanting to combine your three login forms into one, i.e. if a user is in all three tables is there some unique username that links them? Or are you wanting to keep three separate login forms and just use the same login code for each?Wagonette
@Tim no, there are three separate login forms and separate user names / passwords. But one user should be able to be logged in in all three areas at the same time (= using the same browser session).Foresheet
Is Zend_Acl a possible solution? EDIT: Okay now that I read through the question in detail, I see that Zend_Acl doesn't help you, if you want to authenticate against 3 tables.:/Iodine
@faileN Mmm, not really: The three login tables come from separate data sources and should't be unified into one user account.Foresheet
How can you be logged on as more then one identity at the same time? This doesn't make any sense if you're using external data (unless you check for duplicate accounts over all systems)...Norwich
@wim the login tables are completely different groups of users. Think of it as Administrators, Resellers, and end users.Foresheet
Then I don't really see why you don't use a generic user table (unique id, username, password, identity type - to know against what table you have to link to fetch identity specific fields) for all identities that you extend in specific tables for every type of identity (containing unique id and the fields that are specific for that identity).Norwich
@wim the point is that 1) the user data comes from three separate tables and 2) I need to extend Zend_Auth so I can authenticate against each one of the three user groups. At the moment, I can't tell Zend_Auth "Look whether the current user is logged in as a user from table 3". I see what you're getting at and the idea is not bad, but I would like to avoid having a master table. It must be possible to make Zend_Auth "multi-instance capable" - I haven't got around to trying out Keyne's approach yet, maybe that works out.Foresheet
Suppose a user account matches in both your administrators and endusers tables, what should happen then? Will the user be logged in as admin or as enduser? Usually you just login as a single identity (which can have any number of roles attached to it so you can easily define what the user can do using Zend_Acl)... Things would be different - and easy to solve - if you were referring to different methods of login (ie. authenticate through DB, LDAP, OpenID, ...).Norwich
@wim in the example you make, the user will be logged in as admin and as end user, regardless whether the account name matches or not. That's the whole point. I need one user to have different roles from different data.Foresheet
I think my answer will work for you.Embry
K
10

You should create your own Zend_Auth_Adapter. This adapter will try to authenticate against your three resources and will flag it in a private member variable, so you can know which of the logins attempts were sucefully authenticated.

To create your Auth Adapter you can take as basis the Zend_Auth_Adapter_DbTable.

So, in the __construct instead of pass just one DbTable adapter, you can pass the three adapters used in each resource. You'll do in that way only if each one use different resources, like LDAP for example, or even another database, if not, you can pass just one adapter and set three different table names in the configuration options.

Here is the example from Zend_Auth_Adapter_DbTable:

    /**
     * __construct() - Sets configuration options
     *
     * @param  Zend_Db_Adapter_Abstract $zendDb
     * @param  string                   $tableName
     * @param  string                   $identityColumn
     * @param  string                   $credentialColumn
     * @param  string                   $credentialTreatment
     * @return void
     */
    public function __construct(Zend_Db_Adapter_Abstract $zendDb, $tableName = null, $identityColumn = null,
                                $credentialColumn = null, $credentialTreatment = null)
    {
        $this->_zendDb = $zendDb;

        // Here you can set three table names instead of one
        if (null !== $tableName) {
            $this->setTableName($tableName);
        }

        if (null !== $identityColumn) {
            $this->setIdentityColumn($identityColumn);
        }

        if (null !== $credentialColumn) {
            $this->setCredentialColumn($credentialColumn);
        }

        if (null !== $credentialTreatment) {
            $this->setCredentialTreatment($credentialTreatment);
        }
    }

The method bellow, from Zend_Auth_Adapter_DbTable, try to authenticate against one table, you can change it to try in three tables, and for each, when you get sucess, you set this as a flag in the private member variable. Something like $result['group1'] = 1; You'll set 1 for each sucessfully login attempt.

/**
 * authenticate() - defined by Zend_Auth_Adapter_Interface.  This method is called to
 * attempt an authentication.  Previous to this call, this adapter would have already
 * been configured with all necessary information to successfully connect to a database
 * table and attempt to find a record matching the provided identity.
 *
 * @throws Zend_Auth_Adapter_Exception if answering the authentication query is impossible
 * @return Zend_Auth_Result
 */
public function authenticate()
{
    $this->_authenticateSetup();
    $dbSelect = $this->_authenticateCreateSelect();
    $resultIdentities = $this->_authenticateQuerySelect($dbSelect);

    if ( ($authResult = $this->_authenticateValidateResultset($resultIdentities)) instanceof Zend_Auth_Result) {
        return $authResult;
    }

    $authResult = $this->_authenticateValidateResult(array_shift($resultIdentities));
    return $authResult;
}

You will return a valid $authresult only if one of the three login attempts were sucessfully authenticated.

Now, in your controller, after you try to login:

public function loginAction()
{
    $form = new Admin_Form_Login();

    if($this->getRequest()->isPost())
    {
        $formData = $this->_request->getPost();

        if($form->isValid($formData))
        {

            $authAdapter = $this->getAuthAdapter();
                $authAdapter->setIdentity($form->getValue('user'))
                            ->setCredential($form->getValue('password'));
                $result = $authAdapter->authenticate();

                if($result->isValid()) 
                {
                    $identity = $authAdapter->getResult();
                    Zend_Auth::getInstance()->getStorage()->write($identity);

                    // redirect here
                }           
        }

    }

    $this->view->form = $form;

}

private function getAuthAdapter() 
{   
    $authAdapter = new MyAuthAdapter(Zend_Db_Table::getDefaultAdapter());
    // Here the three tables
    $authAdapter->setTableName(array('users','users2','users3'))
                ->setIdentityColumn('user')
                ->setCredentialColumn('password')
                ->setCredentialTreatment('MD5(?)');
    return $authAdapter;    
} 

The key here is the line bellow, that will be implemeted in your custom auth adapter:

$identity = $authAdapter->getResult();

You can take as basis this form Zend_Auth_Adapter_DbTable:

   /**
     * getResultRowObject() - Returns the result row as a stdClass object
     *
     * @param  string|array $returnColumns
     * @param  string|array $omitColumns
     * @return stdClass|boolean
     */
    public function getResultRowObject($returnColumns = null, $omitColumns = null)
    {
        // ...
    }

This return the row matched in the login attempt when sucessfully authenticated. So, you'll create your getResult() method that can return this row and also the $this->result['groupX'] flags. Something like:

public function authenticate() 
{
    // Perform the query for table 1 here and if ok:
    $this->result = $row->toArrray(); // Here you can get the table result of just one table or even merge all in one array if necessary
    $this->result['group1'] = 1;

    // and so on...
    $this->result['group2'] = 1;

    // ...
    $this->result['group3'] = 1;

   // Else you will set all to 0 and return a fail result
}

public function getResult()
{
    return $this->result;
}

After all you can use Zend_Acl to take control over your views and other actions. Since you will have the flags in the Zend Auth Storage, you can use than as roles:

$this->addRole(new Zend_Acl_Role($row['group1']));

Here is some resources:

http://framework.zend.com/manual/en/zend.auth.introduction.html

http://zendguru.wordpress.com/2008/11/06/zend-framework-auth-with-examples/

http://alex-tech-adventures.com/development/zend-framework/61-zendauth-and-zendform.html

http://alex-tech-adventures.com/development/zend-framework/62-allocation-resources-and-permissions-with-zendacl.html

http://alex-tech-adventures.com/development/zend-framework/68-zendregistry-and-authentication-improvement.html

Keane answered 4/8, 2010 at 18:2 Comment(4)
Very nice. I will look into this and if I can work with it, implement it straight away. It could take a few days but I'll definitely be back with feedback.Foresheet
+1 a custom auth adapter would be probably the best idea. for the who can do what on what part use zend_aclDenning
@Foresheet Did you achieve it? Any problems?Keane
I still haven't got around to implementing it yet due to pressing other tasks. I'm awarding you the 350 for being the first to come up with what looks like a complete and viable solution, and for providing great links. I will award a second 350 to either you or wimvds, depending on whose solution worked best for me, in the next few days.Foresheet
N
3

I took some inspiration from the Zym_Auth_Adapter_Chain, but altered it slightly so it doesn't stop on the first adapter that returns successfully.

require_once 'Zend/Auth/Adapter/Interface.php';
require_once 'Zend/Auth/Result.php';

class My_Auth_Adapter_Chain implements Zend_Auth_Adapter_Interface
{
    private $_adapters = array();

    public function authenticate()
    {
        $adapters = $this->getAdapters();

        $results        = array();
        $resultMessages = array();
        foreach ($adapters as $adapter) {
            // Validate adapter
            if (!$adapter instanceof Zend_Auth_Adapter_Interface) {
                require_once 'Zend/Auth/Adapter/Exception.php';
                throw new Zend_Auth_Adapter_Exception(sprintf(
                    'Adapter "%s" is not an instance of Zend_Auth_Adapter_Interface',
                get_class($adapter)));
            }

            $result = $adapter->authenticate();

            if ($result->isValid()) {
                if ($adapter instanceof Zend_Auth_Adapter_DbTable) {
                    $results[] = $adapter->getResultRowObject();
                }
                else {
                    $results[] = $result->getIdentity();
                }
            }
            else {
                $resultMessages[] = $result->getMessages();
            }
        }
        if (!empty($results)) {
            // At least one adapter succeeded, return SUCCESS
            return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $results, $resultMessages);
        }

        return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null, $resultMessages);
    }

    public function getAdapters()
    {
        return $this->_adapters;
    }

    public function addAdapter(Zend_Auth_Adapter_Interface $adapter)
    {
        $this->_adapters[] = $adapter;
        return $this;
    }

    public function setAdapters(array $adapters)
    {
        $this->_adapters = $adapters;
        return $this;
    }
}

To call it from a controller you simply create the chain, then the adapters you want to use (in your case this will probably be a DB adapter per entity table), and finally pass the adapters to the chain.

$db = Zend_Db_Table::getDefaultAdapter();

// Setup adapters
$dbAdminsAdapter = new Zend_Auth_Adapter_DbTable($db, 'admins');    
$dbAdminsAdapter->setIdentityColumn('login')
                ->setCredentialColumn('password')
                ->setIdentity($login)
                ->setCredential($password);

$dbUsersAdapter =  new Zend_Auth_Adapter_DbTable($db, 'users');
$dbUsersAdapter->setIdentityColumn('login')
               ->setCredentialColumn('password')
               ->setIdentity($login)
               ->setCredential($password);
...

// Setup chain
$chain = new My_Auth_Adapter_Chain();
$chain->addAdapter($dbAdminsAdapter)
      ->addAdapter($dbUsersAdapter);

// Do authentication
$auth = Zend_Auth::getInstance();
$result = $auth->authenticate($chain);
if ($result->isValid()) {
    // succesfully logged in
}

This is just basic example code, you probably want to use setCredentialTreatment also on the DbTable adapters...

The upside of this approach is that it will be trivial to add other existing adapters for other forms of authentication (ie. OpenID) to the chain later on...

The downside : as is you will get an array as result from every call to Zend_Auth::getInstance()->getIdentity();. You could of course change this in the Chain adapter, but that's left to you :p.

DISCLAIMER : I really don't think it's wise to do it this way. To make it work you have to share the same login and password accross the different tables, so if a user has more then 1 role (identity) changes his password you'll have to make sure this change is propagated to all identity tables where that user has an account. But I'll stop nagging now :p.

Norwich answered 9/8, 2010 at 15:14 Comment(0)
E
2

Because Zend_Auth is a singleton, creating custom auth adapters for each authentication source only solves the first half of this issue. The second half of the issue is that you want to be able to log in simultaneously with multiple accounts: one of each authentication source.

I asked a similar question recently. The solution was to extend Zend_Auth as shown in the accepted answer. I then initialize the different authentication types in my bootstrap.

protected function _initAuth()
{
    Zend_Registry::set('auth1', new My_Auth('auth1'));
    Zend_Registry::set('auth2', new My_Auth('auth2'));
    Zend_Registry::set('auth3', new My_Auth('auth3'));
}

So, instead of the singleton Zend_Auth::getInstance() you would use Zend_Registry::get('auth1'), etc.

Embry answered 1/12, 2010 at 16:26 Comment(0)
F
1

Why not just create a view that merge all 3 tables, then authenticate against that view?

Foin answered 6/8, 2010 at 2:28 Comment(1)
That won't give me the ability to be logged in in any of the three tables at the same time.Foresheet

© 2022 - 2024 — McMap. All rights reserved.