How to do preDispatch forwarding in Zend Framework?

13,458

Solution 1

I use a combination of a Zend_Controller_Plugin and an AuthController to protect my sites. It supports password resets, forced password changes, and automatic account locking, hence the moderate complexity.

Note that I use Doctrine, so this obviously can't just be cut and pasted into your application, but it should function with minimal changes. I removed a few methods specific to my application, but all the general authentication foo is there.

Plugin

<?php
class Hobo_Controller_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $auth = Zend_Auth::getInstance();
        if ($auth->hasIdentity()) {
            if ('logout' != $request->getActionName()) {
                if (! $request->getParam('force_password_change')
                    && $this->_checkPasswordExpiry($auth->getIdentity()->username)
                ) {
                    $request->setParam('force_password_change', true);
                    $request->setModuleName('default')
                        ->setControllerName('auth')
                        ->setActionName('change-password');
                }
            }
        } else {
            // Defer more complex authentication logic to AuthController
            if ('auth' != $this->getRequest()->getControllerName()) {
                $redirector = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
                $redirector->gotoSimple('restricted', 'auth');
            }
        }
    }

    protected function _checkPasswordExpiry($username)
    {
        // Look up user and return true if password is expired
    }
}

AuthController

<?php

class AuthController extends Zend_Controller_Action
{
    public function init()
    {
        $this->_auth = Zend_Auth::getInstance();

        $this->_errorMessenger = new Zend_Controller_Action_Helper_FlashMessenger();
        $this->_errorMessenger->setActionController($this)->init();
        $this->_errorMessenger->setNamespace('error');

        $this->_noticeMessenger = new Zend_Controller_Action_Helper_FlashMessenger();
        $this->_noticeMessenger->setActionController($this)->init();
        $this->_noticeMessenger->setNamespace('notice');

        $this->view->errors = $this->_errorMessenger->getMessages();
        $this->view->notices = $this->_noticeMessenger->getMessages();
    }

    public function preDispatch()
    {
        if (! $this->_auth->hasIdentity()) {
            if (! in_array($this->_request->getActionName(), array(
                    'logout', 'identify', 'forgot-password', 'reset-password', 'restricted'))
                ) {
                $this->_redirect('/auth/restricted');
            }
        }
    }

    public function restrictedAction()
    {
        // Shows access restricted page
    }

    public function logoutAction()
    {
        $this->_auth->clearIdentity();
        Zend_Session::destroy();
        $this->_redirect('/');
    }

    public function identifyAction()
    {
        if ($this->_request->isPost()) {
            $username = $this->_getParam('username');
            $password = $this->_getParam('password');

            if (empty($username) || empty($password)) {
                $this->_flashError('Username or password cannot be blank.');
            } else {
                $user = new dUser();
                $result = $user->login($username, $password);

                if ($result->isValid()) {
                    $user->fromArray((array) $this->_auth->getIdentity());

                    if ($this->_getParam('changepass') || $user->is_password_expired) {
                        $this->_redirect('auth/change-password');
                        return;
                    }
                    $this->_doRedirect($user);
                    return;
                } else {
                    $this->_doFailure($result->getIdentity());
                }
            }
        }
        $this->_redirect('/');
    }

    public function changePasswordAction()
    {
        if ($this->_request->isPost()) {
            $username = $this->_auth->getIdentity()->username;
            $formData = $this->_request->getParams();

            if (empty($formData['password'])
                || empty($formData['new_password'])
                || empty($formData['confirm_password'])
            ) {
                $this->_flashError('Password cannot be blank.');
                $this->_redirect('auth/change-password');
            } elseif ($formData['new_password'] !== $formData['confirm_password']) {
                $this->_flashError('Password and confirmation do not match.');
                $this->_redirect('auth/change-password');
            } else {
                $user = new dUser();
                $result = $user->login($username, $formData['password']);

                if ($result->isValid()) {

                    $user->updatePassword($username, $formData['new_password']);
                    $this->_flashNotice('Password updated successfully!');
                    $this->_redirect('/');
                } else {
                    $this->_flashError('Invalid username or password!');
                    $this->_redirect('auth/change-password');
                }
            }

        }

        if ($this->_getParam('force_password_change')) {
            $this->view->notice = 'Your password has expired. You must change your password to continue.';
        }
    }

    public function forgotPasswordAction()
    {
        if ($this->_request->isPost()) {
            // Pseudo-random uppercase 6 digit hex value
            $resetCode = strtoupper(substr(sha1(uniqid(rand(),true)),0,6));

            Doctrine_Query::create()
                ->update('dUser u')
                ->set('u.reset_code', '?', array($resetCode))
                ->where('u.username = ?', array($this->_getParam('username')))
                ->execute();

            $this->_doMail($this->_getParam('username'), $resetCode);

            $this->_flashNotice("Password reset request received.");
            $this->_flashNotice("An email with further instructions, including your <em>Reset Code</em>, has been sent to {$this->_getParam('username')}.");
            $this->_redirect("auth/reset-password/username/{$this->_getParam('username')}");
        }
    }

    public function resetPasswordAction()
    {
        $this->view->username = $this->_getParam('username');
        $this->view->reset_code = $this->_getParam('reset_code');

        if ($this->_request->isPost()) {
            $formData = $this->_request->getParams();
            if (empty($formData['username']) || empty($formData['reset_code'])) {
                $this->_flashError('Username or reset code cannot be blank.');
                $this->_redirect('auth/reset-password');
            } elseif ($formData['new_password'] !== $formData['confirm_password']) {
                $this->_flashError('Password and confirmation do not match.');
                $this->_redirect('auth/reset-password');
            } else {
                $user = new dUser();
                $result = $user->loginWithResetCode($formData['username'], $formData['reset_code']);

                if ($result->isValid()) {
                    $user->updatePassword($result->getIdentity(), $formData['new_password']);

                    $user->fromArray((array) $this->_auth->getIdentity());

                    $this->_flashNotice('Password updated successfully!');
                    $this->_doRedirect($user);
                } else {
                    $this->_doFailure($result->getIdentity());
                    $this->_redirect('auth/reset-password');
                }
            }
        }
    }

    protected function _doRedirect($user)
    {
        $this->_helper->Redirector->gotoUserDefault($user);
    }

    protected function _flashError($message)
    {
        $this->_errorMessenger->addMessage($message);
    }

    protected function _flashNotice($message)
    {
        $this->_noticeMessenger->addMessage($message);
    }

    protected function _doFailure($username)
    {
        $user = Doctrine_Query::create()
            ->from('dUser u')
            ->select('u.is_locked')
            ->where('u.username = ?', array($username))
            ->fetchOne();

        if ($user->is_locked) {
            $this->_flashError('This account has been locked.');
        } else {
            $this->_flashError('Invalid username or password');
        }
    }
}

Solution 2

Alternatively you could use the following:

  $request->setParam('skippredispatch',true);
  $this->_forward('index');

In your controller and then [before] this

// If overwriting jump the pre-dispatchy bits
if ($request->getParam('skippredispatch')){
  $request->setParam('skippredispatch',null);
  return;
}

Effectively skipping the predispatch loop which seems to work fine.

Solution 3

This process could be in any action, but why not loginAction, or indexAction in LoginController?

  1. Check for a logged in identity: found? redirect to index
  2. Check for post params: found? validate, set identity or set error messages
  3. Print form

Edit: might've been too tired to realize the real problem. I'd start out with something like a protected/private member in each login-protected controller, such as protected $authNeeded = true;, and check for it in Zend_Controller_Action::init(). This could lead to repeated code, so another option would be to just perform this code in AuthNeededController which all login-protected controller extends.

Share:
13,458
PvB
Author by

PvB

CTO and co-owner of an online marketing company in Berne, Switzerland.

Updated on June 04, 2022

Comments

  • PvB
    PvB almost 2 years

    I want to use _forward() in preDispatch after checking if the user is logged in in every controller.

    The scenario is quite easy: If the user is not logged in, it should be forwarded to loginAction either in the same controller or in another controller.

    This would cause an infinite loop, as the dispatch process starts over again, calling preDispatch again and the forwarding would start everything again.

    The only solution I was able to come up with was to check if the loginAction is already set in the request.

    So my question is, how would the sophisticated developer handle this issue?

    UPDATE Just after hitting the send button, the ghost of the holy awareness came across ;) Another idea would be to build a LoginController to handle the login request. Or is there an even better way?