Welcome to Abdul Malik Ikhsan's Blog

Using zend-expressive-csrf with zend-form in Expressive 3

Posted in expressive, Tutorial PHP, Zend Framework by samsonasik on February 13, 2018

If you followed my post about authentication and authorization posts with Expressive 3, this time, I write another session related post for securing request, which uses zend-expressive-csrf with zend-form.

Setup

We already installed session and form components, so, we can just require the zend-expressive-csrf component via command:

$ composer require \
    zendframework/zend-expressive-csrf:^1.0.0alpha1

On Login route, we register the csrf middleware before the LoginPageHandler:

// config/routes.php
$app->route('/login', [
    // csrf middleware
    \Zend\Expressive\Csrf\CsrfMiddleware::class,

    App\Handler\LoginPageHandler::class,
    \Zend\Expressive\Authentication\AuthenticationMiddleware::class,
], ['GET', 'POST'],'login');

Csrf Validation in Form

We can inject the LoginForm with Zend\Expressive\Csrf\SessionCsrfGuard instance which next we use it to validate csrf token.

// src/App/Form/LoginForm.php
use Zend\Expressive\Csrf\SessionCsrfGuard;

    // ...
    private $guard;

    public function __construct(SessionCsrfGuard $guard)
    {
        parent::__construct('login-form');
        $this->guard = $guard;

        $this->init();
    }
    // ...

In above __construct(), I call init() immediatelly on __construct() to add form elements on form creation, as we are going to inject the Form with Zend\Expressive\Csrf\SessionCsrfGuard instance on LoginPageHandler which pulled from request object.

We then can add new element, for example, named: csrf as hidden input, as follows:

// src/App/Form/LoginForm.php
use Zend\Form\Element\Hidden;

    // ...
    public function init()
    {
        $this->add([
            'type'  => Hidden::class,
            'name'  => 'csrf',
        ]);
        //...
    }
    // ...

To validate it, we register the csrf validation token by Zend\Expressive\Csrf\SessionCsrfGuard in getInputFilterSpecification():

// src/App/Form/LoginForm.php
    // ...
    public function getInputFilterSpecification()
    {
        return [
            [
                'name' => 'csrf',
                'required' => true,
                'validators' => [
                    [
                        'name' => 'callback',
                        'options' => [
                            'callback' => function ($value) {
                                return $this->guard->validateToken($value);
                            },
                            'messages' => [
                                'callbackValue' => 'The form submitted did not originate from the expected site'
                            ],
                        ],
                    ]
                ],
            ],
            // ...
        ];
    }

Above, we supply a callback validator (you can create a special validator just for it) with call the Zend\Expressive\Csrf\SessionCsrfGuard::validateToken($value) which returns true when valid, and false when invalid. On invalid token, we will get callbackValue message key which we can customize its value.

The LoginPageHandler

As the Zend\Expressive\Csrf\SessionCsrfGuard instance will be injected at the LoginPageHandler itself, we can remove the LoginForm from LoginPageHandler dependency, so, we just need to have TemplateRendererInterface:

// src/Handler/LoginPageHandler.php
    // ...
    public function __construct(TemplateRendererInterface $template)
    {
        $this->template  = $template;
    }
    // ...

The factory for it will remove LoginForm dependency as well, so just the template:

// src/Handler/LoginPageFactory.php
    // ...
    public function __invoke(ContainerInterface $container) : MiddlewareInterface
    {
        $template  = $container->get(TemplateRendererInterface::class);
        return new LoginPageHandler($template);
    }

As the token csrf is a one time token, and we use a single page for both GET (show form) and POST (authenticate), we can create a function to get generated token to be called before POST method check, and when authentication failure or the form is invalid to ensure next retry will use newly generated token:

// src/Handler/LoginPageHandler.php
use Zend\Expressive\Session\SessionInterface;
use Zend\Expressive\Csrf\SessionCsrfGuard;

    // ...
    private function getToken(SessionInterface $session, SessionCsrfGuard $guard)
    {
        if (! $session->has('__csrf')) {
            return $guard->generateToken();
        }

        return $session->get('__csrf');
    }
    // ...

Now, the LoginForm and the token can be created during process() method:

// src/Handler/LoginPageHandler.php
use Zend\Expressive\Session\SessionMiddleware;
use Zend\Expressive\Csrf\CsrfMiddleware;
use Zend\Expressive\Csrf\SessionCsrfGuard;

    // ...
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $guard     = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE);
        $loginForm = new LoginForm($guard);

        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
        $token   = $this->getToken($session, $guard);

        // ...
    }

On after form is valid check, the $token need to be re-generated:

// src/Handler/LoginPageHandler.php

    // ...
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        // ...
        if ($loginForm->isValid()) {
            // ...
        }

        // re-new token on failure login or not valid form
        $token   = $this->getToken($session, $guard);
    }

The $token will be consumed by view and can be set via template render:

// src/Handler/LoginPageHandler.php

    // ...
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        // ...
        return new HtmlResponse(
            $this->template->render('app::login-page', [
                'form'  => $loginForm,
                'error' => $error,
                'token' => $token,
            ])
        );
    }

The View

Finally, we can set the csrf element value in view via form object, as follows:

<?php // templates/app/login-page.phtml
echo $error;

$form->get('csrf')->setValue($token);
$form->prepare();

echo $this->form($form);

All done, now, when the request is not using the session generated token, it will show form error:

The form submitted did not originate from the expected site

like the following screenshot:

Better Practice and Possible Refactor

For real application, it is better to use PRG or use another handler to handle it to be redirected back to the form when failure, so you don’t need to tweak the token regeneration as when form re-displayed again, it already in next request and we can just use the new token. I’ve written new post for create middleware for Post/Redirect/Get in Expressive 3 for it.

Advertisements

One Response

Subscribe to comments with RSS.

  1. […] we already explore about CSRF usage in Expressive 3 using zend-expressive-csrf component, which I gave the sample inside a Login Page which utilize […]


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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: