Zend Framework Tutorial Series: Part 3 – Login and Signup with (RE)Captcha

Post Pic

The third part of the tutorial series will present you with a fully functionable solution for a login and signup page that activates a RECaptcha, via Zend_Captcha as a Service, when the user attempts to login/signup 3 times and fails. The tutorial will also show you how to use your models and how to structure your bussiness logic inside your module based application.

Continuing with the tutorial series, we will now see how to manage a login / sign up page with spam prevention via (RE)captcha provided by Zend_Captcha. We will see how to manage validation on the server side, how to contain your business logic in your models, how to prevent multiple submissions and how to communicate with your database without giving your controller any knowledge about your DAO. We will use the Repository and Factory design pattern to be able to easily Unit Test your models and to contain your business logic and database access object into separate layers. We will also see how to insert your dependencies everywhere in your objects to follow the Dependency Injection standards so that your code can be easily unit tested.

1. The database & the database config

Before we start, lets create a database with a table called users.

The table users should contain 3 columns :

  • id – primary key, auto increment, not null, unsigned int
  • username – varchar(128), not null
  • password – varchar(32), not null

Now, in your application.ini, go to your resources.multidb.front_db and change the values to your connection string, as an example:

...
resources.multidb.front_db.adapter  = "pdo_mysql"
resources.multidb.front_db.host     = localhost
resources.multidb.front_db.username = MyDatabaseUsername
resources.multidb.front_db.password = MyDatabasePassword
resources.multidb.front_db.dbname   = MyDatabaseName
resources.multidb.front_db.default  = true
...

2. The layouts

We want to have a separate layout for the logged in users and another layout for guests.

To achieve this, in your /app_root/modules/frontoffice/layouts/ make sure you have 2 files called: “public.phtml” and “layout.phtml” with the following contents:


layout.phtml

	< ?php echo $this->doctype(); ?>
	< html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
	< head>
		< ?php echo $this->headMeta(); ?>
		< ?php echo $this->headTitle(); ?>
		< ?php echo $this->headStyle(); ?>
		< ?php echo $this->headLink(); ?>
		< script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">< /script>
		< ?php echo $this->headScript(); ?>
	< /head>
	< body>
		< div class="container">
		< ?php foreach ($this->messages['error'] as $message):?>
			< div style="color:red;">< ?php echo $message?>< /div>
		< ?php endforeach;?>
		< ?php foreach ($this->messages['success'] as $message):?>
			< div style="color:green;">< ?php echo $message?>< /div>
		< ?php endforeach;?>
		< br />
		< br />
		< p>You are now logged in!< /p>
		< ?php echo $this -> layout () -> content; ?>
		< /div>
	< /body>
	< /html>

and in your public.phtml

	< ?php echo $this->doctype(); ?>
	< html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
	< head>
		< ?php echo $this->headMeta(); ?>
		< ?php echo $this->headTitle(); ?>
		< ?php echo $this->headStyle(); ?>
		< ?php echo $this->headLink(); ?>
		< script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">< /script>
		< ?php echo $this->headScript(); ?>
	< /head>
	< body>
		< div class="container">
		< ?php foreach ($this->messages['error'] as $message):?>
			< div style="color:red;">< /div>
		< ?php endforeach;?>
		< ?php foreach ($this->messages['success'] as $message):?>
			< div style="color:green;">< /div>
		< ?php endforeach;?>
		< br />
		< br />

		< ?php echo $this -> layout () -> content; ?>
		< /div>
	< /body>
	< /html>

This way, we can show a custom UI for the logged in users and another UI for the guests.

3. The Auth Controller Plugin

We now want to make sure that guests are redirected to the users/login page (we will talk about that controller/action in a moment).

Also, if the user is already authenticated, we do not want to allow him on the users/login page and he should be redirected to the index/index page

To achieve this, we will create a controller plugin in the /app_root/library/Custom/Controller/Plugin called Auth.php with the following contents:


class Custom_Controller_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
	/**
	 * @var Zend_Auth
	 */
	protected $_auth;	

	public function __construct(Zend_Auth $auth)
	{
		$this->_auth = $auth;
	}

	public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
	{
		//Check if the user is not logged in
		if (!$this->_auth->hasIdentity())
		{
			return $this->_redirect($request, 'users', 'login', 'frontoffice');
		}

		//The user is logged in
		//Check if the authenticated user tries to access the users/login path
		if ('frontoffice' == $request->getModuleName()
			&& 'users' 		 == $request->getControllerName()
			&& 'login'		 == $request->getActionName())
		{
			return $this->_redirect($request, 'index', 'index', 'frontoffice');
		}
	}

	protected function _redirect($request, $controller, $action, $module)
	{
		if ($request->getControllerName() == $controller
			&& $request->getActionName()  == $action
			&& $request->getModuleName()  == $module)
		{
			return TRUE;
		}

		$url = Zend_Controller_Front::getInstance()->getBaseUrl();
		$url .= '/'   . $module
			 . '/' . $controller
			 . '/' . $action;

	   if (DEBUG)
	   {
	       debug_redirect($url);
	   }

	   return $this->_response->setRedirect($url);
	}
}

To run this plugin we need to go to the Application Bootstrap (the one from the app_root) and add the following function :

//init Auth Plugin
protected function _initAuthPlugin()
{
	Zend_Controller_Front::getInstance()->registerPlugin(
		new Custom_Controller_Plugin_Auth(Zend_Auth::getInstance()));
}

Now this basically tells our application to redirect to the /frontoffice/users/login in case we are not logged in (and it will avoid a redirect loop also if we are already there)

4. The Users Controller

The users controller will allows us to manage a signup and a login action for our guests. The controller will only handle the request and will communicate with the view to render the form and error messages. It will also communicate with the User Repository for user actions (to find out if the user already exists or if the form is valid or not).

Thus, in your /app_root/modules/frontoffice/controllers/ create the file UsersController with the following contents:

class UsersController extends Frontoffice_Library_Controller_Action_Abstract
{
	public function indexAction()
	{
		return $this->redirect('users', 'login');
	}

	public function loginAction()
	{
		Zend_Layout::getMvcInstance()->setLayout('public');

		$form = new Frontoffice_Form_Login(
			array('userRepository' => Frontoffice_Model_Repositories_UsersFactory::factory()));

		if ($this->_request->isPost())
		{
			$data = $this->_request->getPost();
			if ($form->isValid($data))
			{
				return $this->redirect('index', 'index');
			}
			$form->setDefaults($data);
		}

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

	public function logoutAction()
	{
		Zend_Auth::getInstance()->clearIdentity();
		return $this->redirect('users', 'login');
	}
}

This tells our application to redirect to the users/login in case we are trying to access the users/index path. If we are already on the login action, it simply instantiates a new form and injects the Users Repository.

If the request is actually a post and the form is valid with the data provided via the POST method, then it redirects the user to the index/index

Also, the action sets the layout to the public layout since the guest is not allowed to see our logged in UI.

You may ask yourself where are the errors treated. These are set in the form directly (either from form or model validation) and will be displayed in the view from the form so the controller is not aware of them

Also, if we go to users/logout we will be redirected to the users/login and our identity will be deleted

5. The Login View

Lets create the login.phtml view now in your /app_root/modules/frontoffice/views/scripts/users/ with the following contents:

< div style="width:960px; margin:100px auto;">
	< h2>Login header

	< div style="width:500px; margin:5px; padding:0px 20px; float:left">
		< h3>Please login< /h3>

		< ?php if ($this->form->isErrors()):?>
			< ?php foreach ($this->form->getErrors() as $errors):?>
				< ?php foreach ($errors as $message):?>
					< div style="color:red">< /div>
				< ?php endforeach;?>
			< ?php endforeach;?>

			< ?php foreach ($this->form->getErrorMessages() as $error):?>
					< div style="color:red">< /div>
			< ?php endforeach;?>

		< ?php endif; ?>

		< form id="login" action="< ?php echo $this->escape($this->form->getAction()); ?>" method="< ?php echo $this->escape($this->form->getMethod());?>">

			Username: < ?php echo $this->form->username;?>< br />< br />

			Password: < ?php echo $this->form->password;?>< br />< br />

			< ?php echo $this->form->captcha;?>

			< input type="submit" value="Login" /> or < a  href="< ?php echo $this->url(array('controller' => 'users', 'action' => 'signup'), null, true);?>">Sign up< /a>
			< ?php echo $this->form->___h;?>

			< br />
			< br />

		< /form>
	< /div>
< /div>

It is a simple view which renders some form elements and the captcha (the form takes care to enable it or not)

6. The Login Form

The login form will allow us to define the elements we want to show; it will also be injected with the user repository so it can query it for an existent username and fail if it finds one but also user the repository to do the validation. I am not using form validation for elements that contain business logic. As an example, the “Not Empty” validation is done by the form while the requirements for the elements is done by the user repository.

The error messages are set in the form, by both the form and the repository, based on the source of it.

If the form contains errors (either from the element validators or from the business logic validators), they will be shown via the view.

If you the form gives an error 3 times, the captcha will be enabled and will now allow you to pass the form validation until you enter the right captcha code (note here that if you are behind a proxy, the captcha validation will not work)

Now lets create the form class. In your /app_root/modules/frontoffice/forms/ create a file called Login.php with the following contents:

class Frontoffice_Form_Login extends Custom_Form
{
	/**
	 * @var Admin_Model_Repositories_Users
	 */
	protected $_user_repository;

	public function setUserRepository( $user_repository)
	{
		$this->_user_repository = $user_repository;
	}

    public function __construct($options = null)
    {
    	parent::__construct($options);

        $this->setName('login');

        $element = new Zend_Form_Element_Text('username', array('disableLoadDefaultDecorators' => true));
        $element->addDecorator('ViewHelper')
	            ->setRequired(true)
	            ->addErrorMessage('The username is required.');
		 $this->addElement($element);

        $element = new Zend_Form_Element_Password('password', array('disableLoadDefaultDecorators' => true));
        $element->addDecorator('ViewHelper')
                ->setRequired(true)
                ->addErrorMessage('The password is required.');
		$this->addElement($element);

        $element = new Zend_Form_Element_Hash('___h', array('disableLoadDefaultDecorators' => true));
        $element->setSalt('unique')
        		->addDecorator('ViewHelper')
        		->addErrorMessage('Form must not be resubmitted');
        $this->addElement($element);

        $captcha_session = new Zend_Session_Namespace('captcha');

        if ($captcha_session->tries > 999)
        {
	        $recaptcha = new Zend_Service_ReCaptcha('API_KEY_HERE',
	        									    'API_KEY_HERE');
	        $recaptcha->setOption('theme', 'clean');
	        $element = new Zend_Form_Element_Captcha('captcha',
													 array('disableLoadDefaultDecorators' => true,
													 	   'captcha'        => 'ReCaptcha',
														   'captchaOptions' => array('captcha' => 'ReCaptcha',
														   							 'service' => $recaptcha)));
			$element->addErrorMessage('Invalid security captcha code');
			$this->addElement($element);
        }

        $this->clearDecorators();
		$this->addDecorator('FormElements')
	         ->addDecorator('Form');
    }

    public function isValid($data)
    {
    	if (parent::isValid($data))
    	{
    		if ($this->_user_repository->authenticate($data['username'], $data['password']))
    		{
    			Zend_Session::namespaceUnset('captcha');
    			return TRUE;
    		}
    		else
    		{
    			$this->setErrors(array('Invalid username or password'));
    		}
    	}

    	$captcha_session = new Zend_Session_Namespace('captcha');
		if (empty($captcha_session->tries))
		{
			$captcha_session->tries = 0;
		}
		$captcha_session->tries = $captcha_session->tries + 1;
    	return FALSE;
    }
}

7. The Users Repository (Model)

Now lets see what the authenticate method does:

Go into your /app_root/modules/frontoffice/models/Repositories/ and create the file Users.php with the following contents:


class Frontoffice_Model_Repositories_Users
{
	/**
	 * @var Admin_Model_Entities_User
	 */
	protected $_user_entity;

	/**
	 *
	 * @param Admin_Model_Entities_User $user_entity
	 */
	public function __construct($user_entity)
	{
		$this->_user_entity = $user_entity;
	}

	/**
	 * var array
	 */
	protected $_messages = array();

	/**
	 * Sets an error message
	 *
	 * @param string $name
	 * @param string $message
	 *
	 * @return bool
	 */
	public function setMessage($name, $message)
	{
		$this->_messages[$name] = $message;
		return TRUE;
	}

	/**
	 * Returns a list of all error messages
	 *
	 * @return array
	 */
	public function getMessages()
	{
		return $this->_messages;
	}

	/**
	 * Authenticates a user based on the username and password
	 *
	 * @param string $username
	 * @param string $password
	 *
	 * @return boolean
	 */
	public function authenticate($username, $password)
	{
		$filter = new Zend_Validate_StringLength(array('min' => 5, 'max' => 25));
		if (!empty($password) && !$filter->isValid($password))
		{
			$this->setMessage('password', 'Invalid password. Length must be between 5 and 25 characters');
			return FALSE;
		}

    	if (TRUE === $this->_user_entity->loginByUsernameAndPassword($username, $password))
    	{
    		$storage = $this->_user_entity->getResultRowObject(array(
    						'id',
    						'username'));
    		$storage->name = $storage->username;

    		Zend_Session::rememberMe(60 * 60 * 24 * 7 * 2);
    		Zend_Auth::getInstance()->getStorage()->write($storage);

    		return TRUE;
    	}

    	return FALSE;
	}

The Repository uses composition and gets injected with the user entity object which provides the interface to communicate with the database.

The Repository holds the validation of the parameters and manages the relationship between the controller and the database model.

We will have one method called authenticate will use basically run the validation code of the parameters and if it passes the validation, it reaches the User Entity and tries to query it there. It always returns a boolean based on the status.

To call the repository, we will use the factory (below) to get the object. Create a new file in your /app_root/modules/frontoffice/models/Repositories/ named UsersFactory.php with the following contents:

class Frontoffice_Model_Repositories_UsersFactory
{
	/**
	 * @var Frontoffice_Model_Repositories_Users
	 */
	protected static $_repository;

	public static function setRepository($repository)
	{
		self::$_repository = $repository;
	}

	/**
	 * @return Frontoffice_Model_Repositories_Users
	 */
	public static function factory()
	{
		if (null !== self::$_repository)
		{
			return self::$_repository;
		}

		$user_entity = new Frontoffice_Model_Entities_User();

		return new Frontoffice_Model_Repositories_Users($user_entity);
	}
}

8. The User Entity

Now we will see the actual User Entity which provides the interface which communicates with the database itself:

Create a file in your /app_root/modules/frontoffice/models/Entities/ with the name User.php with the following contents:

class Frontoffice_Model_Entities_User extends Custom_Db_Table_Abstract
{
	protected $_name = 'users';
	protected $_use_adapter = 'front_db';

	protected $_auth_adapter;

	const PASSWORD_HASH = 'MY_PASSWORD_HASH_WHICH_SHOULD_BE_SOMETHING_SECURE';

	/**
	 * Logins a user based on his username and password
	 *
	 * @param string $username
	 * @param string $password
	 *
	 * @return Zend_Auth_Result
	 */
	public function loginByUsernameAndPassword($username, $password)
	{
		$password = $this->_encryptPassword($password);

		$this->_auth_adapter = new Zend_Auth_Adapter_DbTable( $this->getAdapter() );
		$this->_auth_adapter->setTableName('users')
						    ->setIdentityColumn('username')
						    ->setCredentialColumn('password');

		$this->_auth_adapter->setIdentity($username)
		    			    ->setCredential($password);
    	$result = Zend_Auth::getInstance()->authenticate($this->_auth_adapter);
    	return $result->isValid();
	}

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

	/**
	 * Encrypts a value by md5 + static token
	 * 10 times
	 *
	 * @param string $value
	 *
	 * @return string $value
	 */
	protected function _encryptPassword($value)
	{
		for ($i = 0; $i < 10; $i++)
		{
			$value = md5($value . self::PASSWORD_HASH);
		}

		return $value;
	}

The User Entity provides methods to C.R.U.D. the database via the Repository.

In our case, we want to be able to run the Zend_Auth_Adapter_DbTable on our database and retrieve our needed information in order to write them in the Auth Storage.

Also we will use a method to encrypt our password va multi md5, in a secure way.

Now if you run the application you should get 5 types of error messages:

  • Username empty
  • Password empty
  • Form cannot be resubmitted
  • Invalid captcha code
  • Invalid username or password (this is due to the fact that a hacker should not know what went wrong with his u/p combination.

To test that the form works correctly check ZFDebug queries and see what it tried to search in the database (the username and password combination - the password will be encrypted. Copy paste that password and add it manually to the database, to your username. Now retry and you should get redirected to the index/index and be logged in)

9. The Sign Up action

Now that we have the login done, lets go to the UsersController and create our signup method:

Add the following code to your UsersController.php


//...
//previous code here
//...
public function signupAction()
{
	Zend_Layout::getMvcInstance()->setLayout('public');

		$user_repository = Frontoffice_Model_Repositories_UsersFactory::factory();
		$form = new Frontoffice_Form_Signup(array('userRepository' => $user_repository));

		if ($this->_request->isPost())
		{
			$data = $this->_request->getPost();
			if ($id = $form->isValid($data))
			{
				$user_repository->authenticate($data['username'], $data['password']);
				return $this->redirect('index','index');
			}
			$form->setDefaults($data);
		}

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

Basically, the code is almost identical (at this level of simplicity atleast) with the login action.

If the form subbmision is correct, make sure to authenticate the user and redirect him to the index/index page.

10. The Sign Up View

Now lets see the View for the Sign Up action:

Go into your app_root/modules/frontoffice/views/scripts/users and create a file called login.phtml with the following contents:

< div style="width:950px; margin:5px; padding:0px 20px; float:left">
	< h2>Create account:< /h2>

	< ?php if ($this->form->isErrors()):?>
		< ?php foreach ($this->form->getErrors() as $errors):?>
			< ?php foreach ($errors as $message):?>
				< div style="color:red">< ?php echo $message;?>< /div>
			< ?php endforeach;?>
		< ?php endforeach;?>

		< ?php foreach ($this->form->getErrorMessages() as $error):?>
				< div style="color:red">< ?php echo $error;?>< /div>
		< ?php endforeach;?>
	< ?php endif; ?>

	< form id="login" action="< ?php echo $this->escape($this->form->getAction()); ?>" method="< ?php echo $this->escape($this->form->getMethod());?>">

	Username: < br />< ?php echo $this->form->username;?>< br />< br />

	Password: < ?php echo $this->form->password;?> < br />< br />

	Confirm Password: < ?php echo $this->form->confirm_password;?>< br />< br />

	< ?php echo $this->form->captcha;?>

	< input type="submit" value="Create account" /> or < a href="< ?php echo $this->url(array('controller' => 'users', 'action' => 'login'), null, true);?>">Login
	< ?php echo $this->form->___h;?>

	< /form>
< /div>

11. The Auth Plugin

Currently, if we try to signup we will always get redirected to the login page. In order to allows us to view the signup page, we must go to the app_root/library/Custom/Controller/Plugin/Auth.php and there we should add the following condition in the dispatchLoopStartup :

	//Check if the user is not logged in
	if (!$this->_auth->hasIdentity())
	{
		return $this->_redirect($request, 'users', 'login', 'frontoffice');
	}

becomes

	//Check if the user is not logged in
	if (!$this->_auth->hasIdentity()
		&& FALSE === (   'frontoffice' == $request->getModuleName()
					  && 'users' 		 == $request->getControllerName()
					  && 'signup'		 == $request->getActionName()))
	{
		return $this->_redirect($request, 'users', 'login', 'frontoffice');
	}

This gives us permissions to view the users/signup page

12. The Users Repository (again)

//previous code here
//...
	/**
	 * Creates a new user
	 *
	 * @param $username
	 * @param $password
	 *
	 * @return bool|int
	 */
	public function createUser($username, $password)
	{
		$filter = new Zend_Validate_StringLength(array('min' => 5, 'max' => 25));
		if (!$filter->isValid($password))
		{
			$this->setMessage('password', 'The password must be between 5 and 25 characters length');
			return FALSE;
		}

		if (FALSE !== $this->_user_entity->findByUsername($username))
		{
			$this->setMessage('username', 'Username already exists');
			return FALSE;
		}

		if (!$id = $this->_user_entity->create($username, $password))
		{
			$this->setMessage(null, 'An uknown error occured. Please contact the support team');
			return FALSE;
		}

		return $id;
	}

This allows us to call the createUser with the username and password and the system checks if the values are correct. We have the logic validation on the repository side while the passowrd/confirm password will be kept on the form side

If the validation passses, we then check to see if we don't already have another user with the same username.

If we don't, we try to create the user with the username and password and if this works we return back the new user id

13. The Users Entity (again)

Go to the /app_root/modules/frontoffice/models/Entities/User.php and add the following code after the previously added one:

	//previous code here
	//...

	/**
	 * Retrieves an User Object by its ID
	 *
	 * @param integer $user_id
	 *
	 * @return Zend_Db_Table_Row_Abstract|bool
	 */
	public function findByUsername($username)
	{
		$result = $this->fetchRow($this->getAdapter()->quoteInto('email = ?', $username));
		if (!empty($result))
		{
			return $result;
		}

		return FALSE;
	}

	/**
	 * Creates a new user object in the database with the specified
	 * column values
	 *
	 * @param string $username
	 * @param string $password
	 *
	 * return bool|integer
	 */
	public function create($username, $password)
	{
		$user = $this->createRow();

		$user->email              = $username;
		$user->password           = $this->_encryptPassword($password);

		try
		{
			$user->save();
			return $user->id;
		}
		catch (Exception $e)
		{
			debug($e);
			return FALSE;
		}
	}

These two methods provide the interface to the Database Object for the Repository via the Entity.

14. Last but not least, The Sign Up Form

The sign up form will be injected with the user repository, will create the username,password/confirm password elemenets, will provide validation for the empty state of the fields + check to see if the 2 password fields are identical and will enable the captcha if the form fails 3 times.

If the form validation is valid, it runs the repository createUser() method. If this validation fails, it will set the form error messages based on the repository messages

If it passes, then it will return the $id of the newly created user

Once the user is created, the controller will just authenticate the user via the standard authenticate() method from the repository with the provided userame and password from the create form.

In your app_path/modules/frontoffice/forms create the file Signup.php with the following contents:


class Frontoffice_Form_Signup extends Custom_Form
{
	/**
	 * @var Frontoffice_Model_Repositories_Users
	 */
	protected $_user_repository;

	/**
	 * Sets the user repository
	 *
	 * @param Frontoffice_Model_Repositories_Users $repository
	 */
	public function setUserRepository($repository)
	{
		$this->_user_repository = $repository;
	}

    public function __construct($options = null)
    {
    	parent::__construct($options);

        $this->setName('create_account');

        $element = new Zend_Form_Element_Text('username', array('disableLoadDefaultDecorators' => true));
        $element->addDecorator('ViewHelper')
	            ->setRequired(true)
	            ->addErrorMessage('Please provide an username value');
		$this->addElement($element);

        $element = new Zend_Form_Element_Password('password', array('disableLoadDefaultDecorators' => true));
        $element->addDecorator('ViewHelper')
                ->setRequired(true)
                ->setAttrib('autocomplete', 'off')
                ->addErrorMessage('Please enter a password');
		$this->addElement($element);

        $element = new Zend_Form_Element_Password('confirm_password', array('disableLoadDefaultDecorators' => true));
        $element->addDecorator('ViewHelper')
                ->setRequired(true)
                ->addValidator(new Frontoffice_Form_Validate_IdenticalFormValues('password'), true)
                ->addErrorMessage('The two passwords do not match');
			$this->addElement($element);

        $element = new Zend_Form_Element_Hash('___h', array('disableLoadDefaultDecorators' => true));
        $element->setSalt('unique')
        		->addDecorator('ViewHelper')
        		->addErrorMessage('Form must not be resubmitted');
        $this->addElement($element);

   		$captcha_session = new Zend_Session_Namespace('captcha');
        if ($captcha_session->tries > 3)
        {
	        $recaptcha = new Zend_Service_ReCaptcha('6LeDkroSAAAAAHAe8FnYK2e9-jbLdAbk8XXn_0UK',
	        									    '6LeDkroSAAAAACLmAdTSdM9sifKJWERFDRUSB0So');
	        $recaptcha->setOption('theme', 'clean');
	        $element = new Zend_Form_Element_Captcha('captcha',
													 array('disableLoadDefaultDecorators' => true,
													 	   'captcha'        => 'ReCaptcha',
														   'captchaOptions' => array('captcha' => 'ReCaptcha',
														   							 'service' => $recaptcha)));
			$element->addErrorMessage('Invalid security captcha code');
			$this->addElement($element);
        }

        $this->clearDecorators();
		$this->addDecorator('FormElements')
	         ->addDecorator('Form');
    }

    public function isValid($data)
    {
    	if (!parent::isValid($data))
    	{
    		$this->_incrementCaptcha();
    		return FALSE;
    	}

    	if (!$id = $this->_user_repository->createUser($data['username'],
    												   $data['password']))
		{
			foreach ($this->_user_repository->getMessages() as $element_name => $message)
			{
				$this->addErrors(array($message));
			}
			$this->_incrementCaptcha();
			return FALSE;
		}

    	return $id;
    }

    protected function _incrementCaptcha()
    {
    	$captcha_session = new Zend_Session_Namespace('captcha');
		if (empty($captcha_session->tries))
		{
			$captcha_session->tries = 0;
		}
		$captcha_session->tries = $captcha_session->tries + 1;
    }
}

However, we are not done yet, since we have a custom validator that check if 2 fields are identical. Thus, lets create, in the path /app_path/modules/frontoffice/forms/Validate the file called IdenticalFormValues.php with the following contents:

class Frontoffice_Form_Validate_IdenticalFormValues extends Zend_Validate_Abstract
{
	const NOT_MATCH = 'notMatch';

	protected $_messageTemplates = array(
    	self::NOT_MATCH => 'Values don\'t match'
	);

	protected $_token_key;

	public function __construct($token_key = 'confirm_password')
	{
		$this->_token_key = $token_key;
	}

	public function isValid($value, $context = null)
	{
    	$value = (string) $value;
    	$this->_setValue($value);

    	if (is_array($context))
    	{
        	if (isset($context[$this->_token_key]) && ($value == $context[$this->_token_key]))
        	{
            	return true;
        	}
    	}
    	elseif ($value == $context)
    	{
        	return true;
    	}

	    $this->_error(self::NOT_MATCH);
	    return false;
	}
}

15. Finale

This was a big tutorial but should pretty much cover everything in order to build a secure signup/login system

See you until next time for the next tutorial!

  • Adrian
    Hey, I have genuinely tried my best but I think I've gone wrong somewhere, if I load up the site I get:

    You are now logged in!
    Hello Frontoffice Index Controller World

    test_valuetest_value_2test_value3

    and clicking login takes me to the login page which then, for obvious reasons, wants to redirect back to index. I never logged in (never even made a username!) and it looks like even though it's not causing an error, the Xml.php script isn't doing much :/ Have I missed something?
  • Hello Adrian,

    Regarding the Xml.php, it does nothing if you provide it with a string.

    If you run the helper with an array as the value, it will print our the <key>value</key> (recursive also).

    Regarding the authentication, it is pretty hard to debug it from here :) but what you CAN do is go to the Auth.php plugin and debug the following:

    Zend_Auth::getInstance()->hasIndentity and getIdentity() .

    You might have the session with an identity from another zend app ? Zend applications that use Zend_Auth share sessions => if you are logged in some app, you will be logged in, in another one, if on the same domain.

    Tell me please if you "have an identity" in the Auth.php (just do debug(Zend_Auth::getInstance()->getIdentity() or hasIdentity() )

    I'll wait for more info's from you.

    Cheers!
  • Adrian
    Just looked at the Xml thing, the views>scripts>index>index.phtml contains $this->xml(array('test' => 'test_value', 'test2' =>....
    and outputs what I wrote above (the Xml.php file is identical to the one in the downloaded files of tutorial 2 as it has a different and what I believe to be the correct class name and extends a different class to the blog text.)

    Will look into the zend auth issue, you may be right there I have been fiddling with a different auth tutorial. Thanks for the hint, i'll let you know!
  • Hello Adrian again,

    Did you by chance view the HTML source? There is a big chance that your browser does not show the xml tags.

    The helper is created by me only to fit the absence of a Zend_Xml::encode() like we have for Json. It is not by any means a substitute for context switch nor will it change the headers of the application to XML.
  • Adrian
    Genius! Yes it's working fine haha didn't think that about an html page!
    On the Auth.php debug, putting the line in the indexController.php returns false for has and nothing for get, I couldn't get the line to do anything putting it anywhere obvious in Auth.php so I hope this was right! (New to zend, so far it seems like the hard way to do everything. Here's me hoping it will pay off!)

    PS also tried in another browser in case it was some kind of cache/session issue, page still behaves the same. :(
  • Hello Adrian,

    I am glad the XML issue has been fixed !

    Regarding the Auth, if it returns FALSE then it seems that there is a bug somewhere. If you go to the Login Controller and put the debug there, anything happens?

    Mind you that if the redirect is trying to apply, your debug will show down the page as a "session" component so perhaps you can scroll down.

    Basically the Auth.php file only checks if the user has an identity (and if not redirects to the login page). So you need to find out :

    a) does the auth.php file detect the user as logged in ?
    b) does the login controller know that the user is logged in ? (both cases check for hasIdentity()
    c) if one of the 2 above say TRUE then you need to go back the control flow and find where the identity is set.

    I am sure its a simple thing. Perhaps you could also remove all code from the login action from the login controller, just to make sure it doesn't run anything without your permissions.

    Regarding Zend and going it the hard way: you won't want to go back once you get to know it better :)

    Cheers!
  • Adrian
    Found the problem! I had a mistake in the Bootstrap file in the application root calling the auth plugin. I suspected it when nothing I did to the Auth.php reflected in the page so thanks for the suggestion!
    I'm silently hoping this will all start clicking together at a rapid pace once I get over the threshold :) Now I just need to work out how to add more modules/controllers myself :)
  • Hello Adrian,

    Hard one but good job you found it ! To add more modules its simple..Just copy your default/frontoffice module, add to each controller the prefix Modulename_SomeCOntroller and make sure nothing points to the other module. You will find more resources about this in the comments below and in the article itself.

    Cheers & good luck!
  • Guest
    Thanks for your great articles :) !
    Could you maybe add colored syntax highlighting?
  • I'm working on that..I am actually testing some syntax highlighting plugins.. All that I've tried so far kind of wreck the code..But I'll find one in the end :)

    Cheers!
  • Hi Andrei, very good tutorial. So fare its giving me some great ideas of different ways to get a decent modular project setup. What i was hoping for for the authentication was a dedicated authentication module, so i can reuse it in other projects, thats what i'm currently working on.
  • Hello Joe,

    That is a bit tricky and without namespaces, it is even harder :) But you gave me a good ideea and will think about it. Perhaps a series of modules that can be reused in any app might come handy.

    Cheers !
  • Bob
    eagerly awaiting the next installment of this series! Really enjoying it, Andrei!
  • Doing my best to provide a detailed next article along side implementing best practices :).

    Next one is complicated.
  • Amino S
    Dear Andrei do you have clue why I am getting this error:
    Fatal error: Call to undefined method ArrayObject::getTraceAsString() in ErrorController.php on line 40

    Am I missing something?
  • its because it should actually be $this->_error->exception->getTraceAsString() - I forgot to add the "exception" :)
  • Roy
    Hi Andrei, I have a question, I have this error produced for the plugin :

    Should redirect to: /frontoffice/users/login
    Called from /var/applications/my_zend_example_application/library/Custom/Controller/Plugin/Auth.php, line 53

    That error is because in /app_root/library/Custom/Controller/Plugin/Auth.php there is a line:

    $url = Zend_Controller_Front::getInstance()->getBaseUrl();

    But $url var is totally emtpy :(

    So Zend_Controller_Front has a function called setBaseUrl($base = null) but I don't know where to used it :( and in your tutorial doesn't mention that :(

    I hope you can help me :)

    Thanks in advance

    Roy
  • It is not an error. The Auth plugin says that you cannot view any page unless you are logged in. Thus it tries to redirect you to the Login page. You can change this via the Auth.php file.

    The method is empty because you are running Zend in your root server.

    The Base Url returns everything after the domain name

    Example:

    http://domain.com/application/myCoolName

    would return via getBaseUrl() "/application/myCoolName"

    but if you have http://domain.com

    it will return ""

    If you need the domain name, just use the $_SERVER['HTTP_HOST']; with http:// or https:// in front .

    If you want to make it dynamic, just use the conditional short if:

    $domain = $_SERVER['HTTPS'] != "on" ? 'http://' : 'https://' . $_SERVER['HTTP_HOST'];
  • Roy
    Thanks man, I got it =D
  • Spellbound85
    Great Tut again... well, did not read all of it yet, but was wondering: In login.phtml you have various loops running through errors but you forgot to echo them...
blog comments powered by Disqus


Popular tags

Partner Blogs

Latest tweets


Get Adobe Flash playerPlugin by wpburn.com wordpress themes
Web Analytics