Using zend-expressive-csrf with zend-form in Expressive 3
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.
[…] 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 […]
[…] we need to generate “csrf” token when rendering the form, like I written at the CSRF usage in Expressive post, we can just use the session as is, and the cache will be used as session […]