In control of HTTPS for action controllers

I work more and more with web applications where users persist personal data and realize the functions to access this data requires a secured connection. Currently I do not use SSL for login, but for next projects I would like to incorporate this into the Soflomo system. [1]

To enable an SSL config with Apache2 on a Linux system (in our case Ubuntu Linux) the Ubuntu wiki provides a very good tutorial how to complete this in a couple of minutes with self-signed certificates. The certificates can be bought by certified organizations of course, but in my development environment, self-singed certificates are good enough. [2]

Next, I want to create the control for http/https environments in the Zend Framework as easy as possible. Therefore I create an action helper for the Zend Framework's action controllers to force pages to be served in https and, when necessary, in http. There are a few assumptions/requirements for this helper:

  1. Determine http/https requirements on action basis
  2. By default, no redirect happens (the action accepts both http and https access)
  3. When necessary, http and https can be forced for specific actions

To fulfill this list, an action helper is created which is small in lines of code, but does exactly what it must do:

class Soflomo_Controller_Action_Helper_Https extends Zend_Controller_Action_Helper_Abstract
    protected $_https = array();
    protected $_http = array();

    public function forceHttps(array $actions)
    {
        $this->_https = $this->_filter($actions);
        return $this;
    }

    public function forceHttp(array $actions)
    {
        $this->_http = $this->_filter($actions);
        return $this;
    }

    public function preDispatch()
    {
        $action = $this->getRequest()->getActionName();
        $scheme = $this->getRequest()->getScheme();
        $url    = $this->getFrontController()->getParam('domainName')
                . $this->getRequest()->getRequestUri();

        if (in_array($action, $this->_https) && $scheme === 'http') {
            $this->getActionController()
                 ->getHelper('redirector')
                 ->gotoUrlAndExit('https://' . $uri);
        } elseif (in_array($action, $this->_http) && $scheme === 'https') {
            $this->getActionController()
                 ->getHelper('redirector')
                 ->gotoUrlAndExit('http://' . $url);
        }
    }

    protected function _filter(array $actions)
    {
        $methods   = get_class_methods($this->getActionController());
        $actionKey = ucfirst($this->getRequest()->getActionKey());

        foreach ($actions as $key => $action) {
            $action = $action . $actionKey;
            if (!in_array($action, $methods)) {
                unset($actions[$key]);
            }
        }

        return $actions;
    }
}

The action helper is initiated by using either the function forceHttps() or forceHttp(). You can pass an array of actions to both functions. The actions are filtered, so it is sure the list consist of actions which are available in the action controller.

At predispatch, the current action and the list of http/https actions are compared. When the current action is in the list of enforced http/https actions and the current scheme does not match, the redirect happens. [3]

Example

This is a small action controller scaffold demonstrating the working principle of the action helper.

class Application_FooController extends Zend_Controller_Action
{
    public function init()
    {
        $this->https->forceHttps(array('bar'))
                    ->forceHttp(array('baz'));
    }

    public function BarAction() {}

    public function BazAction() {}

    public function BatAction() {}
} 

When you visit foo/bar, you get redirected to the https connection. Browsing to foo/baz, you are enforced to use http. And for foo/bat you do not get redirected anyway, because the system is not configured for this action. [4]

Notes

[1] Of course, login is the most common task, but for e.g. e-commerce we have input of address information at the shopping cart and confirmation of payment. In many more occasions the SSL might be handy.

[2] It is likely you want to run multiple applications from your localhost server and therefore you can run into problems with SSL and the Apache2 vhost structure. Use the NameBasedSSLVHostWithSNI directive to enable multiple SSL sites under one ip address (your localhost).

[3] The redirect url ($url) is composed of a domain name and a request uri. For every application I build, the domainName param is set in the application.ini and that is how I fetch the domain name in this case. For various other benefits this param is set, and you can set the param by adding this rule to your application.ini:

resources.frontController.params.domainName = "my.domain.tld"

If you do not want such parameter, you need to change the action helper. For example, you can use the request object to get the server name:

$domainName = $this->getRequest()->getServer('SERVER_NAME');

[4] I simplified the usage of action helpers by extending the Zend_Controller_Action class and add __get() and __call(). With this change you can access action helpers by $this->https or $this->https() in your action controller. I wrote more about this change in an earlier blog post.