Middleware authentication

Many web applications require restricting specific areas to authenticated
users, and may further restrict specific actions to authorized user roles.
Implementing authentication and authorization in a PHP application is often
non-trivial as doing so requires altering the application workflow. For
instance, if you have an MVC design, you may need to change the dispatch logic
to add an authentication layer as an initial event in the execution flow, and
perhaps apply restrictions within your controllers.

Using a middleware approach is simpler and more natural, as middleware easily
accommodates workflow changes. In this article, we will demonstrate how to
provide authentication in a PSR-7 middleware application using
Expressive and
zend-authentication. We
will build a simple authentication system using a login page with username and
password credentials.

Since the content of this post is quite long, we’ll detail authorization
in a separate blog post.

Getting started

This article assumes you have already created an Expressive application. For the
purposes of our application, we’ll create a new module, Auth, in which we’ll
put our classes, middleware, and general configuration.

First, if you have not already, install the tooling support:

$ composer require --dev zendframework/zend-expressive-tooling

Next, we’ll create the Auth module:

$ ./vendor/bin/expressive module:create Auth

With that out of the way, we can get started.

Authentication

The zend-authentication component offers an adapter-based authentication
solution, with both a number of concrete adapters as well as mechanisms for
creating and consuming custom adapters.

The component exposes ZendAuthenticationAdapterAdapterInterface, which
defines a single authenticate() method:

namespace ZendAuthenticationAdapter;

interface AdapterInterface
{
    /**
     * Performs an authentication attempt
     *
     * @return ZendAuthenticationResult
     * @throws ExceptionExceptionInterface if authentication cannot be performed
     */
    public function authenticate();
}

Adapters implementing the authenticate() method perform the logic necessary to
authenticate a request, and return the results via a
ZendAuthenticationResult object. This Result object contains the
authentication result code and, in the case of success, the user’s identity.
The authentication result codes are defined using the following constants:

namespace ZendAuthentication;

class Result
{
    const SUCCESS = 1;
    const FAILURE = 0;
    const FAILURE_IDENTITY_NOT_FOUND = -1;
    const FAILURE_IDENTITY_AMBIGUOUS = -2;
    const FAILURE_CREDENTIAL_INVALID = -3;
    const FAILURE_UNCATEGORIZED = -4;
}

If we want to implement a login page with username and password
authentication, we can create a custom adapter such as the following:

// In src/Auth/src/MyAuthAdapter.php:

namespace Auth;

use ZendAuthenticationAdapterAdapterInterface;
use ZendAuthenticationResult;

class MyAuthAdapter implements AdapterInterface
{
    private $password;
    private $username;

    public function __construct(/* any dependencies */)
    {
        // Likely assign dependencies to properties
    }

    public function setPassword(string $password) : void
    {
        $this->password = $password;
    }

    public function setUsername(string $username) : void
    {
        $this->username = $username;
    }

    /**
     * Performs an authentication attempt
     *
     * @return Result
     */
    public function authenticate()
    {
        // Retrieve the user's information (e.g. from a database)
        // and store the result in $row (e.g. associative array).
        // If you do something like this, always store the passwords using the
        // PHP password_hash() function!

        if (password_verify($this->password, $row['password'])) {
            return new Result(Result::SUCCESS, $row);
        }

        return new Result(Result::FAILURE_CREDENTIAL_INVALID, $this->username);
    }
}

We will want a factory for this service as well, so that we can seed the
username and password to it later:

// In src/Auth/src/MyAuthAdapterFactory.php:

namespace Auth;

use InteropContainerContainerInterface;
use ZendAuthenticationAuthenticationService;

class MyAuthAdapterFactory
{
    public function __invoke(ContainerInterface $container)
    {
        // Retrieve any dependencies from the container when creating the instance
        return new MyAuthAdapter(/* any dependencies */);
    }
}

This factory class creates and returns an instance of MyAuthAdapter.
We may need to pass some dependencies to its constructor, such as a database
connection; these would be fetched from the container.

Authentication Service

We can now create a ZendAuthenticationAuthenticationService
that composes our adapter, and then consume the AuthenticationService in
middleware to check for a valid user. Let’s now create a factory for the
AuthenticationService:

// in src/Auth/src/AuthenticationServiceFactory.php:

namespace Auth;

use InteropContainerContainerInterface;
use ZendAuthenticationAuthenticationService;

class AuthenticationServiceFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new AuthenticationService(
            null,
            $container->get(MyAuthAdapter::class)
        );
    }
}

This factory class retrieves an instance of the MyAuthAdapter service and use
it to return an AuthenticationService instance. The AuthenticationService
class accepts two parameters:

  • A storage service instance, for persisting the user identity. If none is
    provided, the built-in PHP session mechanisms will be used.
  • The actual adapter to use for authentication.

Now that we have created both the custom adapter, as well as factories for the
adapter and the AuthenticationService, we need to configure our application
dependencies to use them:

// In src/Auth/src/ConfigProvider.php:

// Add the following import statement at the top of the classfile:
use ZendAuthenticationAuthenticationService;

// And update the following method:
public function getDependencies()
{
    return [
        'factories' => [
            AuthenticationService::class => AuthenticationServiceFactory::class,
            MyAuthAdapter::class => MyAuthAdapterFactory::class,
        ],
    ];
}

Authenticate using a login page

With an authentication mechanism in place, we now need to create middleware to
render the login form. This middleware will do the following:

  • for GET requests, it will render the login form.
  • for POST requests, it will check for credentials and then attempt to
    validate them.

    • for valid authentication requests, we will redirect to a welcome page
    • for invalid requests, we will provide an error message and redisplay the
      form.

Let’s create the middleware now:

// In src/Auth/src/Action/LoginAction.php:

namespace AuthAction;

use AuthMyAuthAdapter;
use InteropHttpServerMiddlewareDelegateInterface;
use InteropHttpServerMiddlewareMiddlewareInterface as ServerMiddlewareInterface;
use PsrHttpMessageServerRequestInterface;
use ZendAuthenticationAuthenticationService;
use ZendDiactorosResponseHtmlResponse;
use ZendDiactorosResponseRedirectResponse;
use ZendExpressiveTemplateTemplateRendererInterface;

class LoginAction implements ServerMiddlewareInterface
{
    private $auth;
    private $authAdapter;
    private $template;

    public function __construct(
        TemplateRendererInterface $template,
        AuthenticationService $auth,
        MyAuthAdapter $authAdapter
    ) {
        $this->template    = $template;
        $this->auth        = $auth;
        $this->authAdapter = $authAdapter;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        if ($request->getMethod() === 'POST') {
            return $this->authenticate($request);
        }

        return new HtmlResponse($this->template->render('auth::login'));
    }

    public function authenticate(ServerRequestInterface $request)
    {
        $params = $request->getParsedBody();

        if (empty($params['username'])) {
            return new HtmlResponse($this->template->render('auth::login', [
                'error' => 'The username cannot be empty',
            ]));
        }

        if (empty($params['password'])) {
            return new HtmlResponse($this->template->render('auth::login', [
                'username' => $params['username'],
                'error'    => 'The password cannot be empty',
            ]));
        }

        $this->authAdapter->setUsername($params['username']);
        $this->authAdapter->setPassword($params['password']);

        $result = $this->auth->authenticate();
        if (!$result->isValid()) {
            return new HtmlResponse($this->template->render('auth::login', [
                'username' => $params['username'],
                'error'    => 'The credentials provided are not valid',
            ]));
        }

        return new RedirectResponse('/admin');
    }
}

This middleware manages two actions: rendering the login form, and
authenticating the user’s credentials when submitted via a POST request.

You will also need to ensure that you have:

  • Created a login template.
  • Added configuration to map the auth template namespace to one or more
    filesystem paths.

We leave those tasks as an exercise to the reader.

We now need to create a factory to provide the dependencies for this
middleware:

// In src/Auth/src/Action/LoginActionFactory.php:

namespace AuthAction;

use AuthMyAuthAdapter;
use InteropContainerContainerInterface;
use ZendAuthenticationAuthenticationService;
use ZendExpressiveTemplateTemplateRendererInterface;

class LoginActionFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new LoginAction(
            $container->get(TemplateRendererInterface::class),
            $container->get(AuthenticationService::class),
            $container->get(MyAuthAdapter::class)
        );
    }
}

Map the middleware to this factory in your dependencies configuration witin the
ConfigProvider:

// In src/Auth/src/ConfigProvider.php,

// Update the following method to read as follows:
public function getDependencies()
{
    return [
        'factories' => [
            ActionLoginAction::class => ActionLoginActionFactory::class,
            AuthenticationService::class => AuthenticationServiceFactory::class,
            MyAuthAdapter::class => MyAuthAdapterFactory::class,
        ],
    ];
}

Use zend-servicemanager’s ReflectionBasedAbstractFactory

If you are using zend-servicemanager in your application, you could skip the
step of creating the factory, and instead map the middleware to
ZendServiceManagerAbstractFactoryReflectionBasedAbstractFactory.

Finally, we can create appropriate routes. We’ll map /login to the
LoginAction now, and allow it to react to either the GET or POST methods:

// in config/routes.php:
$app->route('/login', AuthActionLoginAction::class, ['GET', 'POST'], 'login');

Alternately, the above could be written as two separate statements:

// in config/routes.php:
$app->get('/login', AuthActionLoginAction::class, 'login');
$app->post('/login', AuthActionLoginAction::class);

Authentication middleware

Now that we have the authentication service and its adapter and the login
middleware in place, we can create middleware that checks for authenticated
users, having it redirect to the /login page if the user is not authenticated.

// In src/Auth/src/Action/AuthAction.php:

namespace AuthAction;

use InteropHttpServerMiddlewareDelegateInterface;
use InteropHttpServerMiddlewareMiddlewareInterface as ServerMiddlewareInterface;
use PsrHttpMessageServerRequestInterface;
use ZendAuthenticationAuthenticationService;
use ZendDiactorosResponseRedirectResponse;

class AuthAction implements ServerMiddlewareInterface
{
    private $auth;

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

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        if (! $this->auth->hasIdentity()) {
            return new RedirectResponse('/login');
        }

        $identity = $this->auth->getIdentity();
        return $delegate->process($request->withAttribute(self::class, $identity));
    }
}

This middleware checks for a valid identity using the hasIdentity() method of
AuthenticationService. If no identity is present, we redirect the redirect
configuration value.

If the user is authenticated, we continue the execution of the next middleware,
storing the identity in a request attribute. This facilitates consumption of the
identity information in subsequent middleware layers. For instance, imagine you
need to retrieve the user’s information:

namespace AppAction;

use InteropHttpServerMiddlewareDelegateInterface;
use InteropHttpServerMiddlewareMiddlewareInterface as ServerMiddlewareInterface;
use PsrHttpMessageServerRequestInterface;

class FooAction
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $user = $request->getAttribute(AuthAction::class);
        // $user will contains the user's identity
    }
}

The AuthAction middleware needs some dependencies, so we will need to create
and register a factory for it as well.

First, the factory:

// In src/Auth/src/Action/AuthActionFactory.php:

namespace AuthAction;

use InteropContainerContainerInterface;
use ZendAuthenticationAuthenticationService;
use Exception;

class AuthActionFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new AuthAction($container->get(AuthenticationService::class));
    }
}

And then mapping it:

// In src/Auth/src/ConfigProvider.php:


// Update the following method to read as follows:
public function getDependencies()
{
    return [
        'factories' => [
            ActionAuthAction::class => ActionAuthActionFactory::class,
            ActionLoginAction::class => ActionLoginActionFactory::class,
            AuthenticationService::class => AuthenticationServiceFactory::class,
            MyAuthAdapter::class => MyAuthAdapterFactory::class,
        ],
    ];
}

Like the LoginActionFactory above, you could skip the factory creation and
instead use the ReflectionBasedAbstractFactory if using zend-servicemanager.

Require authentication for specific routes

Now that we built the authentication middleware, we can use it to protect
specific routes that require authentication. For instance, for each route that
needs authentication, we can modify the routing to create a pipeline that
incorporates our AuthAction middleware early:

$app->get('/admin', [
    AuthActionAuthAction::class,
    AppActionDashBoardAction::class
], 'admin');

$app->get('/admin/config', [
    AuthActionAuthAction::class,
    AppActionConfigAction::class
], 'admin.config');

The order of execution for the middleware is the order of the array elements.
Since the AuthAction middleware is provided as the first element, if a user is
not authenticated when requesting either the admin dashboard or config page,
they will be immediately redirected to the login page instead.

Conclusion

There are many ways to accommodate authentication within middleware
applications; this is just one. Our goal was to demonstrate the ease with which
you may compose authentication into existing workflows by creating middleware
that intercepts the request early within a pipeline.

You could certainly make a number of improvements to the workflow:

  • The path to the login page could be configurable.
  • You could capture the original request path in order to allow redirecting to
    it following successful login.
  • You could introduce rate limiting of login requests.

These are each interesting exercises for you to try!

As noted in the introduction, this article demonstrates only authentication.
Stay tuned for a future article that will demonstrate authorization middleware
using zend-permissions-rbac.

Save the date!

Want to learn more about Expressive and Zend Framework? What better location
than ZendCon 2017! ZendCon will be hosted 23-26 October 2017 in Las Vegas,
Nevada, USA. Visit the ZendCon website for more
information
.

Source: Zend feed