Welcome to Abdul Malik Ikhsan's Blog

Create Login functionality in Expressive 3

Posted in expressive, Zend Framework by samsonasik on January 12, 2018

Zend Expressive 3 is not released yet released, and expressive session related components are in active development ready to use. However, we already can give them a try.

Use case

For example, we need simple login functionalities:

  1. Login Form
  2. Authentication process, read from DB
  3. Save authenticated value to Session

Setup

First, we can install the Zend Expressive 3 skeleton with the following command:

$ composer create-project "zendframework/zend-expressive-skeleton:^3.0.0" expressive3

There are components that can be installed via command:

$ cd expressive3
$ composer require \
     zendframework/zend-form:^2.11 \
     zendframework/zend-i18n:^2.7 \
     zendframework/zend-expressive-authentication:^0.4.0 \
     zendframework/zend-expressive-authentication-session:^0.4.1 \
     zendframework/zend-expressive-session:^1.0.0 \
     zendframework/zend-expressive-session-ext:^1.1.1

After above components installed, ensure that your config/config.php injected with ConfigProvider like below:

<?php
// config/config.php
$aggregator = new ConfigAggregator([
    // ... form requirements
    \Zend\I18n\ConfigProvider::class,
    \Zend\Form\ConfigProvider::class,
    \Zend\InputFilter\ConfigProvider::class,
    \Zend\Filter\ConfigProvider::class,
    \Zend\Hydrator\ConfigProvider::class,
    // ...

    // ... auth requirements
    \Zend\Expressive\Authentication\ConfigProvider::class,
    \Zend\Expressive\Authentication\Session\ConfigProvider::class,
    \Zend\Expressive\Session\ConfigProvider::class,
    \Zend\Expressive\Session\Ext\ConfigProvider::class,
    // ...
];

we can first setup database data, in this case, I tried with Postgresql:

$ createdb -Udeveloper expressive
Password:

$ psql -Udeveloper expressive
Password for user developer:

psql (10.1)
Type "help" for help.

expressive=# CREATE TABLE users(username character varying(255) PRIMARY KEY NOT NULL, password text NOT NULL);
CREATE TABLE

expressive=# CREATE EXTENSION pgcrypto;
CREATE EXTENSION

expressive=# INSERT INTO users(username, password) VALUES('samsonasik', crypt('123456', gen_salt('bf')));
INSERT 0 1

Above, I create database named “expressive”, create table named “users” with username and password field, insert sample data with pgcrypto extension for create hashed password of 123456 using blowfish.

Now, we can setup the authentication configuration at config/autoload/local.php as follows:

<?php
// config/autoload/local.php
return [

    'authentication' => [
        'pdo' => [
            'dsn'   => 'pgsql:host=localhost;port=5432;dbname=expressive;user=developer;password=xxxxx',
            'table' => 'users',
            'field' => [
                'identity' => 'username',
                'password' => 'password',
            ],
        ],
        'username' => 'username',
        'password' => 'password',
    ],

];

Then, we can map Zend\Expressive\Authentication\UserRepositoryInterface::class to Zend\Expressive\Authentication\UserRepository\PdoDatabase::class under alias and register Zend\Expressive\Authentication\AuthenticationInterface::class under factories config at config/autoload/dependencies.global.php :

<?php
// config/autoload/dependencies.global.php
return [

    'dependencies' => [
        'aliases' => [
            // ...
            Zend\Expressive\Authentication\UserRepositoryInterface::class =>
                Zend\Expressive\Authentication\UserRepository\PdoDatabase::class
        ],

        'factories' => [
            // ...
            Zend\Expressive\Authentication\AuthenticationInterface::class =>
                Zend\Expressive\Authentication\Session\PhpSessionFactory::class,
        ],
        // ...
    ],

];

For Session operations, we need Zend\Expressive\Session\SessionMiddleware middleware before routing middleware, so, in config/pipeline.php, we call pipe on it before $app->pipe(RouteMiddleware::class);:

<?php
// config/pipeline.php
// ...
use Zend\Expressive\Session\SessionMiddleware;

// ...
$app->pipe(SessionMiddleware::class);

// Register the routing middleware in the middleware pipeline
$app->pipe(RouteMiddleware::class);
// ...

as example: we want to redirect non-logged user to /login page, eg: at home page (/), we can register “home” routes config:

<?php
// config/routes.php
$app->get('/', App\Handler\HomePageHandler::class, 'home');

and in HomePageHandler, we can check:

<?php
// src/App/Handler/HomePageHandler.php
declare(strict_types=1);

namespace App\Handler;

// ...
use Zend\Diactoros\Response\RedirectResponse;
use Zend\Expressive\Authentication\UserInterface;
use Zend\Expressive\Session\SessionMiddleware;
// ...

class HomePageHandler implements RequestHandlerInterface
{
    // ...
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
        if (! $session->has(UserInterface::class)) {
            return new RedirectResponse('/login');
        }

        // ...
    }
}

When access ‘/’ page, we should be redirected to /login page which currently a 404 page, nice!

Login Page

First, we create a LoginForm with username and password field like the following:

<?php
// src/App/Form/LoginForm.php
declare(strict_types=1);

namespace App\Form;

use Zend\Form\Element\Password;
use Zend\Form\Element\Text;
use Zend\Form\Form;
use Zend\InputFilter\InputFilterProviderInterface;

class LoginForm extends Form implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('login-form');
    }

    public function init()
    {
        $this->add([
            'type' => Text::class,
            'name' => 'username',
            'options' => [
                'label' => 'Username',
            ],
        ]);

        $this->add([
            'type' => Password::class,
            'name' => 'password',
            'options' => [
                'label' => 'Password',
            ],
        ]);

        $this->add([
            'name' => 'Login',
            'type' => 'submit',
            'attributes' => [
                'value' => 'Login',
            ],
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            [
                'name' => 'username',
                'required' => true,
                'filters' => [
                    ['name' => 'StripTags'],
                    ['name' => 'StringTrim'],
                  ],
            ],

            [
                'name' => 'password',
                'required' => true,
                'filters' => [
                    ['name' => 'StripTags'],
                    ['name' => 'StringTrim'],
                ],
            ],
        ];
    }
}

We then can create a login page handler with inject it with login form with the following factory:

<?php
// src/App/Handler/LoginPageFactory.php
declare(strict_types=1);

namespace App\Handler;

use App\Form\LoginForm;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
use Zend\Form\FormElementManager;

class LoginPageFactory
{
    public function __invoke(ContainerInterface $container) : MiddlewareInterface
    {
        $template  = $container->get(TemplateRendererInterface::class);
        $loginForm = $container->get(FormElementManager::class)
                               ->get(LoginForm::class);

        return new LoginPageHandler($template, $loginForm);
    }
}

The LoginPageHandler itself can be initialized with :

<?php
// src/App/Handler/LoginPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use App\Form\LoginForm;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\RedirectResponse;
use Zend\Expressive\Authentication\UserInterface;
use Zend\Expressive\Session\SessionMiddleware;
use Zend\Expressive\Template\TemplateRendererInterface;

class LoginPageHandler implements MiddlewareInterface
{
    private $template;
    private $loginForm;

    public function __construct(
        TemplateRendererInterface $template,
        LoginForm                 $loginForm
    ) {
        $this->template  = $template;
        $this->loginForm = $loginForm;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
        if ($session->has(UserInterface::class)) {
            return new RedirectResponse('/');
        }

        $error = '';
        // handle authentication here Next

        return new HtmlResponse(
            $this->template->render('app::login-page', [
                'form'  => $this->loginForm,
                'error' => $error,
            ])
        );
    }
}

Above, we redirect to ‘/’ page when there is a session data as it already authenticated check. We are going to add authentication process next.

The Login form can be as simple as the following:

<?php // templates/app/login-page.phtml

echo $error;

$form->prepare();
echo $this->form($form);

We can register the LoginPageHandler at App\ConfigProvider::getDependencies() config:

<?php
// src/App/ConfigProvider.php
class ConfigProvider
{
    public function getDependencies() : array
    {
        return [
            'invokables' => [ /**/ ],
            'factories'  => [
                // ...
                Handler\LoginPageHandler::class => Handler\LoginPageFactory::class,
            ],
        ];
    }
}

The routing can be registered as follows with add \Zend\Expressive\Authentication\AuthenticationMiddleware::class for next middleware:

// config/routes.php
// ...
$app->route('/login', [
    App\Handler\LoginPageHandler::class,
    // for authentication next handling
    \Zend\Expressive\Authentication\AuthenticationMiddleware::class,
], ['GET', 'POST'],'login');

Above, we allow ‘GET’ and ‘POST’ in same ‘/login’ page.

Authentication process

Time for authentication process, we utilize Zend\Expressive\Authentication\AuthenticationMiddleware class that registered at the last entry at the /login route, we can accomodate it after check of form is valid

<?php
// src/App/Handler/LoginPageHandler.php
class LoginPageHandler implements MiddlewareInterface
{
    // ...
    public function __construct(
        TemplateRendererInterface $template,
        LoginForm                 $loginForm) { /* */ }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ) : ResponseInterface
    {
        // .......
        $error  = '';
        if ($request->getMethod() === 'POST') {
            $this->loginForm->setData($request->getParsedBody());
            if ($this->loginForm->isValid()) {
                $response = $handler->handle($request);
                if ($response->getStatusCode() !== 302) {
                    return new RedirectResponse('/');
                }

                $error = 'Login Failure, please try again';
            }
        }
        // ...

        return new HtmlResponse(
            $this->template->render('app::login-page', [
                'form'  => $this->loginForm,
                'error' => $error,
            ])
        );
    }

We call handle($request) for next Zend\Expressive\Authentication\AuthenticationMiddleware with:

$response = $handler->handle($request);
if ($response->getStatusCode() !== 302) {
    return new RedirectResponse('/');
}

When status code is not 301 302, it authenticated and session filled, we can then redirect to page that need to be authenticated to be access. Failure authentication default behaviour has 301 302 status code which we can set config “redirect” in “authentication” config, on above code, I just want to show it in the login form that the login failure, so I set the $error variable value to “Login Failure, please try again”, so when login failure, it will got the error like the following:

That’s it ;).

How about logout ? We can use clear() method from SessionMiddleware::SESSION_ATTRIBUTE attribute like the following:

use Zend\Expressive\Authentication\UserInterface;
use Zend\Expressive\Session\SessionMiddleware;

class LogoutPageHandler implements RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
        if ($session->has(UserInterface::class)) {
            $session->clear();
        }
        // ...
    }
}

How about authorization part? You can read my next post about create authorization functionality in zend expressive 3

12 Responses

Subscribe to comments with RSS.

  1. mister_bludgeon said, on January 12, 2018 at 10:23 pm

    Thanks for an interesting post. Being not even a novice with Expressive, I’m not yet familiar with the wiring, so I ask: in the snippet that comes after “We call handle($request) for next Zend\Expressive\Authentication\AuthenticationMiddleware with:”, what file would that live in?

    • samsonasik said, on January 13, 2018 at 12:24 am

      the next is a `\Zend\Expressive\Authentication\AuthenticationMiddleware` class which it passed at “login” route at last entry config value, the `\Zend\Expressive\Authentication\ConfigProvider::class` that registered at `config/config.php` handle its service registration.

  2. […] yesterday, I already posted about Authentication part in “Create Login functionality in Expressive 3” post, so, it’s time for authorization part. If you didn’t read that, please read […]

  3. […] you followed my post about authentication and authorization posts with Expressive 3, this time, I write another session related post for […]

  4. […] you already followed my 4 previous expressive posts, all requirements already […]

  5. Juan said, on June 28, 2018 at 2:28 am

    I get the following error: Service with name “Zend\Expressive\Authentication\AuthenticationInterface” could not be created. Reason: The redirect configuration is missing for authentication.

    • samsonasik said, on July 1, 2018 at 2:11 pm

      I retried with run “composer update” to get latest dependencies, and it just works! You probably missing something in the steps, please follow slowly.

    • fancybuddy said, on August 16, 2018 at 9:39 pm

      Of course you have to provide the key ‘redirect’ in the [‘config’][‘authentication’] configuration file as described in Zend\Expressive\Authentication\Session\PhpSessionFactory (where the AuthenticationInterface is mapped in)

      The key is used in case of unauthorizedResponse

  6. caesar_z said, on July 18, 2018 at 9:29 pm

    Hello? Have you had the project code, folder or structure? I have the following error: “Service with name “App\Handler\LoginPageHandler” could not be created. Reason: A plugin by the name “App\Form\LoginForm” was not found in the plugin manager Zend\Form\FormElementManager\FormElementManagerV3Polyfill”

    • samsonasik said, on July 30, 2018 at 2:31 pm

      as in setup described in the post, you need to install `zend-form` and ensure `\Zend\Form\ConfigProvider::class` registered in the ConfigAggregator. By that, the `App\Form\LoginForm` will got auto discover via FormElementManager.

      • James said, on August 3, 2018 at 1:10 am

        Am having the same issue and have read through a couple times. Could you explain this?
        My zend expressive skeloton has src/App/src as the main folder

      • samsonasik said, on August 3, 2018 at 9:46 am

        do you use `zend-servicemanager` and `zend-view` ? If you’re using both components for container and template engine, that should works.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: