Home > Zend Framework > Leveraging Zend_Auth for building your authentication

Leveraging Zend_Auth for building your authentication

In a series of posts, I will address the issue of authentication and authorization of users into your application. When you build a website with any form of back office, you will need to grant users access to the back office (authentication), and determine what actions they are allowed to take (authorization). The Zend Framework has two tools just for that job: Zend_Auth and Zend_ACL. In this first part, I will build a custom User class, that will allow the programmer to perform a simple authentication of a user. This User class will be integrated into a so called “code base” or “framework” that you can use for your own applications. As of this moment, that framework doesn’t exist yet. I will gradually build it as my blog posts are added. Please look for the tag “codebase” if you want all of these posts.

First things first: if you have read my previous posts about securing your Flex – PHP services, you will have seen that I have wrapped the bootstrapping process of that application in a class named “CB_Application”. That class is still under very heavy development. Since the release of Zend Framework 1.8, they have introduced Zend_Application, just for that purpose. So it is very likely that in the future, I will replace my CB_Application class with something that makes use of Zend_Application. For now, I will focus on the other functionality, and leave CB_Application as it is, without bothering you with the details. But full disclosure will come, somewhere in the future.

Structure

I haven’t decided on some kind of structure for this framework yet. I think it will evolve as I add more classes and functionality to it. Below is a screenshot of how it currently looks like.

Codebase Structure

Codebase Structure

As you can see, quite empty. What we will be constructing during this blog post, is the CB_User class, the CB_Table_Users class and the CB_Auth_Adapter_Chap class. Once that is complete, you should include the Code Base folder into your include paths in your bootstrap. If that is done, you can just simple use your framework anywhere in your models or controllers.

Database tables for the User class

I’m going to concentrate on the authentication of the users, so I will keep the amount of data small. It will be the base for the User class, and will be extended with functionality and other data as the framework evolves. Right now, the only properties for a user we need, are: a username, a password, an e-mail address, and a flag to indicate if the user is active or not.

1
2
3
4
5
6
7
8
CREATE TABLE IF NOT EXISTS `cb_users` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) NOT NULL,
  `password` varchar(255) NOT NULL,
  `email` varchar(255) NOT NULL,
  `active` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

Small remark:All passwords are encrypted via the AES encryption method. I won’t use a hashing algorithm, because during the authentication process, I would like to know the exact password. In case I want to build PHP services for Flex, I will need that password, and I can’t come from the client directly. But that’s nothing to worry about now; just remember that the password will be encrypted via AES.

For testing purposes, we will add a dummy user via SQL now. Normally this will be done during some sort of registration process, but that’s for the future. Let’s add the user “test” with password “password” into the database. As key for the AES encryption, I have used “c0d3ba53″. You know, l33t speak as they say ;) You should remember that key. I will hard code it into the code, as this is just a demonstration. In my real code, I have this key in a configuration file, and it is fetched by CB_Application. (I know, it’s irritating, but it will come up here an there during this blog post. Sorry!)

1
INSERT INTO cb_users (username,password,email,active) VALUES ('test','S		í‹ôÚ×ýfÕ‘ˆ?	','test@example.com',1);

Building the authentication

Authenticating a user will be very straightforward. There is only 1 condition: I want to be able to use CHAP and regular authentication. CHAP requires 3 parameters (username, challenge, signature) and regular authentication requires only 2 (username, password). I will incorporate that into 1 authentication function that will dynamically decide which authentication we will be doing, via the amount of parameters.

As I said before, the authentication process will make use of the Zend_Auth component of the Zend Framework. There are many different ways you could authenticate a user. Most commonly is against a database. Zend_Auth can accommodate to all these different ways, by making use of adapters. An auth adapter basically does the authentication, and Zend_Auth uses it. There are already some predefined adapters, but for the CHAP authentication, we will have to create our own.

To access all the data from the cb_users table in the database, We extend the regular Zend_Db_Table class with our own information:

1
2
3
4
class CB_Table_Users extends Zend_Db_Table_Abstract {
	protected $_name = 'cb_users';
	protected $_primary = "id";
}

Nothing fancy, we just specify our table name, and the primary key, and then we’re good to go.

Before we can do the authentication, we first need to create our custom CHAP authentication adapter. The Zend Framework is designed to be extensible, and there is in interface for creating your own authentication adapters. The only method you need to implement is the authenticate() method. But you can add other methods if you deem that necessary. I will implement some setters for the credentials, and then a method that mimics the behaviour of the getResultRowObject() method from Zend_Auth_Adapter_DbTable. I added this extra functionality, because that will make it easier to process everything in the CB_User class. I have overdone myself documenting all the code, so I won’t explain it in depth anymore. If you are considering using this code, please replace all references to CB_application with your own code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
 * CB_Auth_Adapter_Chap class
 *
 * @author Tom Van Herreweghe
 */
 
class CB_Auth_Adapter_Chap implements Zend_Auth_Adapter_Interface {
    private $_username = '';
    private $_signature = '';
    private $_challenge = '';
 
    private $_data = null;
 
    /**
     * Set the username
     *
     * @param string $username
     */
    public function setUsername($username){
        $this->_username = $username;
        return $this;
    }
 
    /**
     * Set the challenge, used to calculate the signature
     *
     * @param string $challenge
     */
    public function setChallenge($challenge){
        $this->_challenge = $challenge;
        return $this;
    }
 
    /**
     * Set the client-side calculated signature
     *
     * @param string $signature
     */
    public function setSignature($signature){
        $this->_signature = $signature;
        return $this;
    }
 
    /**
     * Authenticate the user
     *
     * @return Zend_Auth_Result
     */
    public function authenticate(){
        // First, we need to retrieve the data, associated with the given username.
        // The password will be decrypted for later use:
        $table = new CB_Table_Users();
        $select = $table->select()->from(CB_application::getInstance()->getTablePrefix("cb") . "users",array("id","username","email",CB_application::getInstance()->getDbAdapter()->quoteInto("AES_DECRYPT(password,?) AS password",CB_application::getInstance()->getKey())));
        $select->where("username = ?",$this->_username)->where("active = 1");
        $result = $table->fetchAll($select);
 
        $authenticated = false;
        // If we found a match, verify the other credentials
        if (count($result) == 1){
            $password = $result[0]->password;
            $this->_data = $result[0]->toArray(); // store the database data locally
 
            // Compare calculated signature with given signature:
            $calcSignature = md5(md5($password) . $this->_challenge);
 
            // if the given signature and the calculated signature match, the user is authenticated:
            $authenticated = ($calcSignature == $this->_signature);
        }
        // We prefill the authentication result with a FAILURE
        $authResult = Zend_Auth_Result::FAILURE_UNCATEGORIZED;
        $authMessages = array();
        if ($authenticated){
            // user is authenticated, overwrite the auth result:
            $authResult = Zend_Auth_Result::SUCCESS;
        } else {
            // Couldn't authenticate the user, set a message:
            $authMessages[] = 'Login failed';
        }
        // return the result:
        return new Zend_Auth_Result( $authResult, $this->_username, $authMessages );
    }
 
    /**
     * Mimics (actually almost copies) the behaviour of the getResultRowObjec() from Zend_Auth_Adapter_DbTable
     *
     * @param array $returnColumns
     * @param array $omitColumns
     * @return stdClass
     */
    public function getResultRowObject($returnColumns = null, $omitColumns = null){
        // If no data is set, return false:
        if (!$this->_data) {
            return false;
        }
 
        $returnObject = new stdClass();
 
        if (null !== $returnColumns) {
            // Process only the specified columns:
 
            $availableColumns = array_keys($this->_data);
            foreach ( (array) $returnColumns as $returnColumn) {
                if (in_array($returnColumn, $availableColumns)) {
                    $returnObject->{$returnColumn} = $this->_data[$returnColumn];
                }
            }
            return $returnObject;
 
        } elseif (null !== $omitColumns) {
            // Process all columns, except the ones specified:
 
            $omitColumns = (array) $omitColumns;
            foreach ($this->_data as $resultColumn => $resultValue) {
                if (!in_array($resultColumn, $omitColumns)) {
                    $returnObject->{$resultColumn} = $resultValue;
                }
            }
            return $returnObject;
 
        } else {
            // Process all columns, without restrictions:
 
            foreach ($this->_data as $resultColumn => $resultValue) {
                $returnObject->{$resultColumn} = $resultValue;
            }
            return $returnObject;
        }
    }
}

The puzzle is almost complete now. Let’s put the pieces together in the CB_User class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
 * CB_User class
 *
 * @author Tom Van Herreweghe
 */
 
class CB_User {
    private $data = array();
 
    /**
     * Getter magical function to get properties
     *
     * @param string $key
     * @return mixed
     */
    public function __get($key){
        if (array_key_exists($key,$this->data)){
            return $this->data[$key];
        }
        throw new Exception("Unknown property '{$key}'");
    }
 
    /**
     * Setter magical function to set properties
     *
     * @param string $key
     * @param mixed $value
     */
    public function __set($key,$value){
        $allowed = array("id","username","email");
        if (in_array($key,$allowed)){
            $this->data[$key] = $value;
        }
    }
 
    /**
     * Authenticate a user via credentials
     * The amount of parameters is variable, and are therefore not explicitly declared
     *
     * @return Zend_Auth_Result
     */
    public static function authenticate(){
        // Get all the arguments:
        $args = func_get_args();
        // Initialize an authAdapter, but set it to null
        $authAdapter = null;
 
        if (count($args) == 2){
            // 2 arguments: We assume they are username & password. This means regular authentication
            $authAdapter = new Zend_Auth_Adapter_DbTable(
                CB_application::getInstance()->getDbAdapter(), // This returns an instance of a database adapter
                CB_application::getInstance()->getTablePrefix("cb").'users', // this will return a prefix, defined in a config.
                'username',
                'password'
            );
            // set the necessary credentials
            $authAdapter
                ->setIdentity($args[0])
                ->setCredential($args[1])
                ->setCredentialTreatment("AES_ENCRYPT(?, '".CB_application::getInstance()->getKey()."') AND active = 1");
                // CredentialTreatment says we'll use AES_Encrypt to encrypt the given password, and the user must be active.
                // CB_application::getInstance()->getKey() returns a predefined key from a config file.
        }
        if (count($args) == 3){
            // 3 arguments: We assume username, challenge and signature. This means custom digest authentication
 
            // Here, we use our custom created CB_Auth_Adapter_DbTable:
            $authAdapter = new CB_Auth_Adapter_Chap(
                    CB_application::getInstance()->getDbAdapter(),
                    CB_application::getInstance()->getTablePrefix("cb").'users',
                    'username',
                    'password'
            );
            // set the necessary credentials:
            $authAdapter
                ->setUsername($args[0])
                ->setChallenge($args[1])
                ->setSignature($args[2]);
        }
 
        if (is_null($authAdapter)){
            throw new Exception("No authAdapter specified. Please check all the arguments");
        }
 
        // Zend_Auth will now try to authenticate, using our auth adapter:
        $result = Zend_Auth::getInstance()->authenticate($authAdapter);
 
        if($result->isValid()) {
            /*
             * When authentication is succesfull, we will write an instance of CB_User to the storage
             * Later on, when we want to know which user is authenticated, we will retrieve this CB_User from the storage
             * This all hapens quite transparently via regular Zend Framework code
             */
 
            // get the resulting line from the database, except the "password" column:
            $data = $authAdapter->getResultRowObject(null,'password');
            // retrieve an an instance of CB_User via the user id:
            $user = self::getInstance($data->id);
            // write our CB_User object to the storage of Zend_Auth
            // Zend_Auth::getInstance()->getIdentity() will return that CB_User object
            Zend_Auth::getInstance()->getStorage()->write($user);
        }
        // Return the Zend_Auth result:
        return $result;
    }
 
    /**
     * Return an instance of the CB_User class, via the user_id
     *
     * @param int $user_id
     * @return CB_User
     */
    public static function getInstance($user_id){
        $table = new CB_Table_Users();
        $select = $table->select()->from(CB_application::getInstance()->getTablePrefix("cb") . "users",array("id","username","email"));
        $select->where("id = ?",$user_id)->where("active = 1");
        $result = $table->fetchRow($select);
 
        if (!is_null($result)){
            return self::factory($result);
        }
        throw new Exception("No user found");
    }
 
    /**
     * Transform the raw data from the database into a CB_User object
     *
     * @param Zend_Db_Table_Row $data
     * @return CB_User
     */
    public static function factory($data){
        if (is_a($data,"Zend_Db_Table_Row")){
            $user = new CB_User();
            $user->id       = $data->id;
            $user->username = $data->username;
            $user->email    = $data->email;
            return $user;
        }
        throw new Exception("Wrong datatype for the CB_User::factory() method");
    }
}

Now that we have written our classes, we want to use them. Mind you, this is just a demonstration. We will first log the user in via the CHAP method, then we do a logout, and then we log the user in via the regular method.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class IndexController extends Zend_Controller_Action{
    public function indexAction() {
        $this->getHelper("Layout")->disableLayout();
        $this->getHelper("ViewRenderer")->setNoRender();
 
        // Verify if we aren't logged in yet
        if (Zend_Auth::getInstance()->hasIdentity()){
            // Oops, we were still logged in from a previous demonstration..
            // Let's log out:
            Zend_Auth::getInstance()->clearIdentity();
        }
        // Alright, at this point we're not logged in.. Let's proceed:
 
        $challenge = 'challenge'; // <- this should be a random string
        $password = 'password';
        // Calculate the signature:
        $signature = md5(md5($password) . $challenge);
        // Authenticate via CHAP:
        $result = CB_User::authenticate('test',$challenge,$signature);
        // Show the result
        print_r(Zend_Auth::getInstance()->getIdentity());
 
        // Logout again for another demonstration:
        Zend_Auth::getInstance()->clearIdentity();
        // Lets try the other authentication method:
        $result = CB_User::authenticate('test',$password);
        print_r(Zend_Auth::getInstance()->getIdentity());
        die("demonstration is over !");
        }
}

That’s it for now. We have completed a first part of a framework: create a User class, and allow a user to authenticate. Right now, we can’t do much with it yet, but it will come in handy. We still need a way of registering users, but most importantly, we have to determine what that user can do now that he’s logged in. I will write about that in another blog post about Zend_ACL.

Share and Enjoy:
  • DZone
  • del.icio.us
  • StumbleUpon
  • Digg
  • Ma.gnolia
  • Technorati
  • TwitThis

Tom Zend Framework , ,

  1. Lasse
    May 16th, 2009 at 15:00 | #1

    Once again another great blog post, however I am curious to know why you chose to encrypt instead of hashing the password? – Why would you need to know the password to authenticate a flex user? Couldn’t you just use CHAP as explained in one of your previous posts? As I see it, the biggest problem using a hashing algorithm, is that when you don’t know the exact password coming from the user, you can’t salt the password and then hash. So if an attacker where to gain access to the users database, then a simple dictionary attack would get him the password.

    But then again, if he does gain access to the database then he might also be able to get the encryption/decryption key, thus making it even easier to retrieve the password.

    What are your thoughts?

  2. May 17th, 2009 at 10:24 | #2

    Hi Lasse,

    We could use hashing to obfuscate the passwords. But then if you want to authenticate a user via CHAP, the client would need to know the original salt in order to create the same hash from the password as the hash that is in the database.
    It is fairly simple to decompile a flex project, and get the salt. That would make it easier for a hacker, because then he would only have to hack the database, and use the salt to + dictionary attack to figure out passwords.

    If you decrypt it, the hacker can’t decompile the flex application. He will have to hack both the database server and webserver, making it more complex. But once he managed that, it would again be very easy for him to decipher the passwords, like you said.

    On top of that, I just wanted to mention that the most used hashing algorithms (MD5 & SHA-1) were proven to be vulnerable. This doesn’t mean you can’t use it any more. But it’s probably a good idea to not use those any more for salting passwords.

  3. Lasse
    May 17th, 2009 at 18:59 | #3

    Yes, isn’t SHA512/256 considered the most secure hashing algorithm these days?

    So to conclude; by using CHAP we achieve safe transportation of the password over an unencrypted http connection, but we also sacrifice safe storage of the password in the database? Or at least it is less safe than using hashing and a random salt for each password/user.

    Agree?

  4. Lasse
    May 22nd, 2009 at 14:12 | #4

    ?

  5. May 22nd, 2009 at 14:46 | #5

    Hi Lasse,

    I totally forgot about your comment.

    You are right when saying that with CHAP you achieve safe transportation of the password.
    The design of the database and the encryption I used isn’t relevant to CHAP. I don’t see any security problem with using AES for 2-way encrypting the password in the database. Sure, if you have the key, you can decrypt it. If someone finds the key to your house, he can also easily open the door. It’s up to you to safely store the key.

    I have stored that key in an XML file. Now I just have to make sure that the XML file is safe. A lot can be achieved via the .htaccess file and setting permissions of the directory correct.

    When using CHAP, you could substitute the MD5 hashes for SHA256 hashes. These are indeed more secure. It’s up to you to decide what kind of hashing function you wish to use. I just gave you the idea of how things can be done :)

  6. May 23rd, 2009 at 22:31 | #6

    Hi, nice posts there :-) thank’s for the interesting information

  7. Kirsten
    July 23rd, 2009 at 15:17 | #7

    Hi Tom,

    I found your details on linkedin.
    I work as recruiter for Harvey nash and I’m looking for the moment a PHP developer with experience in the Zend framework.
    The position in Brussels.
    Are you intrested or do you know someone who can be intrested? Or is someone reading this that can be intrested :)

    Best regards,
    Met vriendelijke groetjes,
    Bien à vous,

    Kirsten
    Kirsten.steurs@harveynash.com
    02/463.34.29

  1. May 8th, 2009 at 00:37 | #1