Create Middleware for Post/Redirect/Get in Expressive 3
Yesterday, 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 both GET and POST in single page, which we need to tweak the csrf regeneration to ensure next retry will use newly regenerated token. As I noted in the last paragraph, the better usage is by using PRG ( Post/Redirect/Get ) or delegate to separate middleware and redirect back to its page. Today, we will explore how to create Post/Redirect/Get Middleware for Expressive 3.
Requirements
If you already followed my 4 previous expressive posts, all requirements already applied.
The New Middleware
We are going to create a middleware for our application, for example, we name it PrgMiddleware
, placed at src/App/Middleware
. I will explain part by part.
First, we check whether the request method is POST, then save the POST data into session with new key, eg: ‘post_data’, then redirect to current page with status code = 303.
use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Session\SessionMiddleware; // ... $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($request->getMethod() === 'POST') { $session->set('post_data', $request->getParsedBody()); return new RedirectResponse($request->getUri(), 303); } // ...
On next flow, we can check if the session has ‘post_data’ key:
if ($session->has('post_data')) { $post = $session->get('post_data'); $session->unset('post_data'); $request = $request->withMethod('POST'); $request = $request->withParsedBody($post); } // ...
As in authentication process, we use Zend\Expressive\Authentication\Session\PhpSession::authenticate()
which require request method to be POST and use its parsed body to be used for authentication, we need to set method to POST with parsed body of saved session with key ‘post_data’ which immediately removed above.
Lastly, we return the $handler->handle($request) as whenever response it used:
return $handler->handle($request);
The complete middleware class can be as follows:
<?php // src/App/Middleware/PrgMiddleware.php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Session\SessionMiddleware; class PrgMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($request->getMethod() === 'POST') { $session->set('post_data', $request->getParsedBody()); return new RedirectResponse($request->getUri(), 303); } if ($session->has('post_data')) { $post = $session->get('post_data'); $session->unset('post_data'); $request = $request->withMethod('POST'); $request = $request->withParsedBody($post); } return $handler->handle($request); } }
PrgMiddleware Service Registration
We can register the PrgMiddleware
at src/App/ConfigProvider
under getDependencies()
function:
// src/App/ConfigProvider.php use Zend\ServiceManager\Factory\InvokableFactory; // ... public function getDependencies() : array { return [ 'factories' => [ // ... Middleware\PrgMiddleware::class => InvokableFactory::class, ], ]; }
Now, we can register it inside config/routes.php
for login route after csrf middleware registrations:
// config/routes.php $app->route('/login', [ // csrf \Zend\Expressive\Csrf\CsrfMiddleware::class, // prg middleware App\Middleware\PrgMiddleware::class, // login page App\Handler\LoginPageHandler::class, // authentication middleware \Zend\Expressive\Authentication\AuthenticationMiddleware::class, ], ['GET', 'POST'],'login');
To make clear “post_data” executed, place the App\Middleware\PrgMiddleware::class
at the very first in target redirect, eg: home route:
$app->get('/', [ // prg handling App\Middleware\PrgMiddleware::class, \Zend\Expressive\Authentication\AuthenticationMiddleware::class, App\Handler\HomePageHandler::class, ], 'home');
Using The Prg in LoginPageHandler
We can check if it is a PRG using the parsed body and use it to fill the form data:
// src/App/Handler/LoginPageHandler.php // ... public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $guard = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE); $loginForm = new LoginForm($guard); $prg = $request->getParsedBody(); if ($prg) { $loginForm->setData($prg); if ($loginForm->isValid()) { // ... } } }
By above, we just need to generate the token once. The complete LoginPageHandler
can be as follow:
<?php // src/App/Handler/LoginPageHandler.php declare(strict_types=1); namespace App\Handler; use App\Form\LoginForm; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Csrf\CsrfMiddleware; use Zend\Expressive\Flash\FlashMessageMiddleware; use Zend\Expressive\Session\SessionMiddleware; use Zend\Expressive\Template\TemplateRendererInterface; class LoginPageHandler implements MiddlewareInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $guard = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE); $loginForm = new LoginForm($guard); $prg = $request->getParsedBody(); if ($prg) { $loginForm->setData($prg); if ($loginForm->isValid()) { $response = $handler->handle($request); $flashMessages = $request->getAttribute(FlashMessageMiddleware::FLASH_ATTRIBUTE); if ($response->getStatusCode() !== 302) { $flashMessages->flash('message', 'You are succesfully authenticated'); return new RedirectResponse('/'); } $flashMessages->flash('message', 'Login Failure, please try again'); return new RedirectResponse('/login'); } } $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $token = $guard->generateToken(); return new HtmlResponse( $this->template->render('app::login-page', [ 'form' => $loginForm, 'token' => $token, ]) ); } }
In view, we can just show the form:
<?php // templates/app/login-page.phtml $form->get('csrf')->setValue($token); $form->prepare(); echo $this->form($form);
Done 😉
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.
Using View Helper for Accessing zend-expressive-flash Messages in Expressive 3
If you already tried zend-expressive 3.0.0alpha3, and want to use flash message for it, you can use the new component: “zendframework/zend-expressive-flash”.
Setup
We need to require the component and its dependencies:
$ composer require zendframework/zend-expressive-session:^1.0.0 \ zendframework/zend-expressive-session-ext:^1.0.0 \ zendframework/zend-expressive-flash:^1.0.0
After it, we can ensure that the ConfigProvider
for above components registered at config/config.php
:
$aggregator = new ConfigAggregator([ // ... \Zend\Expressive\Session\ConfigProvider::class, \Zend\Expressive\Session\Ext\ConfigProvider::class, \Zend\Expressive\Flash\ConfigProvider::class, // ... ]);
To make the flash messages available in all pages, we need to register it in the pipeline before RouteMiddleware
piping:
use Zend\Expressive\Flash\FlashMessageMiddleware; use Zend\Expressive\Session\SessionMiddleware; // ... $app->pipe(SessionMiddleware::class); $app->pipe(FlashMessageMiddleware::class); $app->pipe(RouteMiddleware::class);
Get the Messages via View Helper
Most of the use cases, we are displaying the flash messages in the layout. If we are using zend-view
for the template engine, we can create view helper to access the flash messages. What we need is a Zend\Expressive\Flash\FlashMessages
instance with passes Zend\Expressive\Session\Session
which passes $_SESSION
into it if session status is active, as follows:
use Zend\Expressive\Session\Session; use Zend\Expressive\Flash\FlashMessages; // ... $session = session_status() === PHP_SESSION_ACTIVE ? $_SESSION : []; $flashMessages = FlashMessages::createFromSession(new Session($session));
The messages
is a private property “currentMessages” which currently no public function that returns it. We can wait for the PR for it to be merge or use it via ReflectionProperty
or Closure
which can be called by method:
$flashMessages->getFlashes();
Knowing that, we can create the view helper as follows:
<?php // src/App/View\Helper\Flash.php declare(strict_types=1); namespace App\View\Helper; use Zend\Expressive\Session\Session; use Zend\Expressive\Flash\FlashMessages; use Zend\View\Helper\AbstractHelper; class Flash extends AbstractHelper { public function __invoke() : array { $session = session_status() === PHP_SESSION_ACTIVE ? $_SESSION : []; $flashMessages = FlashMessages::createFromSession( new Session($session) ); return $flashMessages->getFlashes(); } }
and register it into view_helpers
config at src/App/ConfigProvider::__invoke()
:
// ... public function __invoke() : array { return [ 'dependencies' => $this->getDependencies(), 'templates' => $this->getTemplates(), // register additional view helpers 'view_helpers' => [ 'invokables' => [ 'flash' => View\Helper\Flash::class, ], ], ]; } // ...
Accessing Flash Messages in Layout
In layout, we can now access it via invoked flash
view helper with loop the messages to be displayed:
<?php if ($flashes = $this->flash()) :?> <ul> <?php foreach($flashes as $message) : ?> <li><?php echo $message; ?></li> <?php endforeach; ?> </ul> <?php endif; ?>
Give it a try
You can now create a flash messages in some page and access it in next request in the layout, check the documentation for its usage!
9 comments