Handling an initial login

When you have configured the adapter, you can drop in the zend-expressive-authentication AuthenticationMiddleware anywhere you need to ensure you have an authenticated user. However, how do you handle the initial authentication?

In the previous chapter, we indicated that you need to configure a path to which to redirect when the adapter does not detect a user. In this chapter, we'll detail how to create a login handler for processing user credentials.

Roughly, what we need to do is:

  • Create a handler that will both display and handle a login form, redirecting to the originally requested location once a successful authentication occurs.

  • Create a template with a form for capturing the username and password.

  • Create a route to the new handler.

Create the handler

We will use the zend-expressive CLI tooling to generate our handler, as well as the related factory and template:

$ ./vendor/bin/expressive handler:create "App\Login\LoginHandler"

By default, if you have a configured template engine, this will do the following:

  • Create the handler for you.

  • Create logic in the handler to render a template and return the contents in a response.

  • Create a factory for the handler.

  • Create a template for you in an appropriate directory.

When it does these things, it provides you with the paths to each as well. In our case, we are using the PlatesPHP templating integration, with a flat application structure, and the following files were either created or updated:

  • src/App/Login/LoginHandler.php, which contains the handler class itself.

  • src/App/Login/LoginHandlerFactory.php, which contains the factory for the handler.

  • config/autoload/zend-expressive-tooling-factories.global.php, which maps the handler to its factory for the DI container.

  • templates/app/login.phtml, which contains our template.

Now that we have created the handler, we can edit it to do the work we need.

Our handler will react to two different HTTP methods.

For an initial login request, the GET method will be used, and we will need to display our template. When we do, we will also memoize the originally requested URI (using the Referer request header).

When the user submits the form, it will be via the POST method. When this happens, we will need to validate the submitted credentials; we will do this using the PhpSession adapter from this package. If login is successful, we will redirect to the originally requested URI, using the value we previously stored in our session. If login fails, we will display our template, adding an error message indicating the credentials were invalid.

The generated handler will already compose the TemplateRendererInterface, and render a template. We will need to add a constructor dependency on the PhpSession adapter, and store that value in a property. Additionally, since we will be performing a redirect for successful POST requests, we will need to add a requirement on Zend\Diactoros\Response\RedirectResponse in addition to the logic changes in the handler.

The end result should look like this:

namespace App\Login;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\RedirectResponse;           // add this line
use Zend\Expressive\Authentication\Session\PhpSession;  // add this line
use Zend\Expressive\Session\SessionInterface;           // add this line
use Zend\Expressive\Authentication\UserInterface;       // add this line
use Zend\Expressive\Template\TemplateRendererInterface;


class LoginHandler implements RequestHandlerInterface
{
    private const REDIRECT_ATTRIBUTE = 'authentication:redirect';

    /** @var PhpSession */
    private $adapter;

    /** @var TemplateRendererInterface */
    private $renderer;

    public function __construct(TemplateRendererInterface $renderer, PhpSession $adapter)
    {
        $this->renderer = $renderer;
        $this->adapter = $adapter;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $session  = $request->getAttribute('session');
        $redirect = $this->getRedirect($request, $session);

        // Handle submitted credentials
        if ('POST' === $request->getMethod()) {
            return $this->handleLoginAttempt($request, $session, $redirect);
        }

        // Display initial login form
        $session->set(self::REDIRECT_ATTRIBUTE, $redirect);
        return new HtmlResponse($this->renderer->render(
            'app::login',
            []
        ));
    }

    private function getRedirect(
        ServerRequestInterface $request,
        SessionInterface $session
    ) : string {
        $redirect = $session->get(self::REDIRECT_ATTRIBUTE);

        if (! $redirect) {
            $redirect = $request->getHeaderLine('Referer');
            if (in_array($redirect, ['', '/login'], true)) {
                $redirect = '/';
            }
        }

        return $redirect;
    }

    private function handleLoginAttempt(
        ServerRequestInterface $request,
        SessionInterface $session,
        string $redirect
    ) : ResponseInterface {
        // User session takes precedence over user/pass POST in
        // the auth adapter so we remove the session prior
        // to auth attempt
        $session->unset(UserInterface::class);

        // Login was successful
        if ($this->adapter->authenticate($request)) {
            $session->unset(self::REDIRECT_ATTRIBUTE);
            return new RedirectResponse($redirect);
        }

        // Login failed
        return new HtmlResponse($this->renderer->render(
            'app::login',
            ['error' => 'Invalid credentials; please try again']
        ));
    }
}

With these changes in place, our handler is now ready. However, we need to update our factory, as we've added a new dependency!

To do this, run the following from the command line, in the project root directory:

$ rm src/App/Login/LoginHandlerFactory.php
$ ./vendor/bin/expressive factory:create "App\Login\LoginHandler"

This will regenerate the factory for you.

Edit the template

We will now edit the template. The main considerations are:

  • It needs to have a form that submits back to the login page.

  • The form needs both a username and a password field.

  • We need to display an error message if one was provided.

Our application is built off the skeleton, and so we are currently using Bootstrap for a UI framework. We are also using PlatesPHP as noted earlier. As such, we will update the template in templates/app/login.phtml to read as follows:

<div class="container">
    <div class="row">
        <div class="col-sm"><form action="<?= $this->url('login') ?>" method="post">
            <?php if (isset($error)) : ?>
            <div class="alert alert-danger" role="alert">
                <?= $this->escapeHtml($error) ?>
            </div>
            <?php endif ?>

            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" class="form-control" id="username" name="username" placeholder="Enter username">
            </div>

            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" class="form-control" id="password" name="password" placeholder="Password">
            </div>

            <button type="submit" class="btn btn-primary">Submit</button>
        </form></div>
    </div>
</div>

Template location and structure

Keep in mind the following when reading the above sample:

  • If you are using the modular structure, the template may be in a different location. Use the output from the expressive handler:create command to determine the exact location.

  • If you are using a different template engine, the syntax of the template may vary.

  • The HTML may need to vary based on your own site's UI framework and CSS.

Create the route

Now that we have the handler and template created, we need to create a route for the new handler that handles two HTTP methods: GET for displaying the initial form, and POST for validating submitted credentials. Open up your config/routes.php file, and edit it to add the following within its callback:

$app->route(
    '/login',
    [
        Zend\Expressive\Session\SessionMiddleware::class,
        App\Login\LoginHandler::class,
    ],
    ['GET', 'POST'],
    'login'
);

Understanding the routing

You may not be familiar with the route() method, or middleware pipelines. If the above doesn't make sense, keep reading for an explanation.

First, we are using the route() method, as we want to create a single route to respond to multiple HTTP methods. This method has a required third argument, which is an array of HTTP methods; we specify GET and POST in this array.

Second, we are indicating that we want the route to respond to the exact path /login; we provide this via the initial method argument.

Third, we are providing a name for this route via the optional fourth argument; this is what allows us to call $this->url('login') in our template in order to generate the URL to the login page.

Finally, for the middleware argument, we are providing a pipeline, by providing an array of middleware to execute. The first item in the pipeline is the SessionMiddleware from zend-expressive-session; this is required to ensure we have a session container injected into the request. The second item is our login handler itself, which will then do the actual work of creating a response.

With this route in place, any routes we write that require authentication will now:

  • Redirect to the /login page, which will require that:

  • A user provides credentials and submits the form back to the /login page, which will:

  • Process the credentials via the PhpSession adapter, which will store identified user details in the session, and:

  • Ultimately give them access (assuming any roles associated with them are authorized), and:

  • Redirect them back to the originally requested page.

In the next chapter, we will detail how to require authentication for individual handlers.

Found a mistake or want to contribute to the documentation? Edit this page on GitHub!