Create Authorization functionality in Expressive 3
So, 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 first.
We will use role
of user when deciding what access right of the user for accessed page. For example, we define role
field in users table with the following SQL:
expressive=# ALTER TABLE users ADD COLUMN role character varying(255) NOT NULL DEFAULT 'user'; ALTER TABLE
Ok, we have new column named role
with default value = ‘user’. So, we have existing data with role = ‘user’ :
expressive=# SELECT * FROM users; username | password | role ------------+--------------------------------------------------------------+------ samsonasik | $2a$06$uPvOqYT7fQFP5EYR2jzVrOefwU03GltjAHt.q8l1vWXmkTIbeBcHe | user
Let’s add another user with different role, eg: ‘admin’, as follows:
expressive=# INSERT INTO users(username, password, role) VALUES('admin', crypt('123456', gen_salt('bf')), 'admin'); INSERT 0 1 expressive=# SELECT * FROM users; username | password | role ------------+--------------------------------------------------------------+------- samsonasik | $2a$06$uPvOqYT7fQFP5EYR2jzVrOefwU03GltjAHt.q8l1vWXmkTIbeBcHe | user admin | $2a$06$0pLYG/GVQOL6v9tLmjBB..cvUIk0vBdcDM8aV373AVO3ve9MdSbom | admin (2 rows)
Perfect, now, we need to add sql_get_roles
config under [‘authentication’][‘pdo’] to get role from users table, we can add at our config/autoload/local.php
:
<?php // config/autoload/local.php return [ 'authentication' => [ 'pdo' => [ // ... 'sql_get_roles' => 'SELECT role FROM users WHERE username = :identity' ], // ... ], ];
When we login and var_dump the session data, we will get the following array value:
// var_dump($session->get(UserInterface::class)); array (size=2) 'username' => string 'samsonasik' (length=10) 'roles' => array (size=1) 0 => string 'user' (length=4)
Until here we are doing great!
To differentiate access page, let’s create a different page for admin
only, for example: AdminPageHandler
:
<?php declare(strict_types=1); namespace App\Handler; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Expressive\Template\TemplateRendererInterface; class AdminPageHandler implements RequestHandlerInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function handle(ServerRequestInterface $request) : ResponseInterface { return new HtmlResponse($this->template->render('app::admin-page', [])); } }
with factory as follows:
<?php declare(strict_types=1); namespace App\Handler; use Psr\Container\ContainerInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Expressive\Template\TemplateRendererInterface; class AdminPageFactory { public function __invoke(ContainerInterface $container) : RequestHandlerInterface { $template = $container->get(TemplateRendererInterface::class); return new AdminPageHandler($template); } }
We can register the AdminPageHandler
middleware at App\ConfigProvider::getDependencies()
:
<?php class ConfigProvider { public function getDependencies() : array { return [ 'invokables' => [ /**/ ], 'factories' => [ // ... Handler\AdminPageHandler::class => Handler\AdminPageFactory::class, ], ]; } }
Then let’s define route for it, eg: ‘/admin’:
// config/routes.php $app->route('/admin', [ \Zend\Expressive\Authentication\AuthenticationMiddleware::class, App\Handler\AdminPageHandler::class, ], ['GET'], 'admin');
The view can just show it that it is currently at admin
page:
<?php // templates/app/admin-page.phtml ?> Admin Page
So, if we logged in as role = user, and access `/admin’ page, we still can see the page.
Let’s authorize it!
First, we can add components for it, for example, we are going to use ACL, we can install expressive component for it via command:
$ composer require \ zendframework/zend-expressive-authorization:^1.0 \ zendframework/zend-expressive-authorization-acl:^1.0
It will install the following components:
* zendframework/zend-expressive-authorization
* zendframework/zend-permissions-acl
* zendframework/zend-expressive-authorization-acl
After they installed, ensure our config/config.php
has registered the following ConfigProvider
classes:
<?php // config/config.php $aggregator = new ConfigAggregator([ // ... \Zend\Expressive\Authorization\Acl\ConfigProvider::class, \Zend\Expressive\Authorization\ConfigProvider::class, // ... ]);
Then, we can map Zend\Expressive\Authorization\AuthorizationInterface::class
to Zend\Expressive\Authorization\Acl\ZendAcl::class
at config/autoload/dependencies.global.php
under alias
to use the ZendAcl
service :
<?php // config/autoload/dependencies.global.php return [ 'dependencies' => [ 'aliases' => [ // ... Zend\Expressive\Authorization\AuthorizationInterface::class => Zend\Expressive\Authorization\Acl\ZendAcl::class ], ], ];
Roles, Resources, and Rights definitions
We can define roles, resources, and rights under [‘zend-expressive-authorization-acl’] config, for example, at config/autoload/zend-expressive.global.php
:
<?php // config/autoload/zend-expressive.global.php return [ // ... 'zend-expressive-authorization-acl' => [ 'roles' => [ 'guest' => [], 'user' => ['guest'], 'admin' => ['user'], ], 'resources' => [ 'home', 'admin', 'login', 'logout', ], 'allow' => [ 'guest' => [ 'login', ], 'user' => [ 'logout', 'home', ], 'admin' => [ 'admin', ], ], ], // ... ];
I’m going to mark non-logged user with role = “guest”, “user” role will inherit all guest rights, and “admin” role inherit all user rights, that mean, admin can access what user can access, but not opposite.
The resources are route names that registered at config/routes.php
.
Authorization Process
To get ‘roles’ value, we have Zend\Expressive\Authorization\AuthorizationMiddleware
that checks from request attribute named Zend\Expressive\Authentication\UserInterface::class
, we can define at config/pipeline.php
before $app->pipe(DispatchMiddleware::class); with continue pipe the Zend\Expressive\Authorization\AuthorizationMiddleware
after it, as follow:
// config/pipeline.php $app->pipe(new class implements Psr\Http\Server\MiddlewareInterface{ public function process( Psr\Http\Message\ServerRequestInterface $request, Psr\Http\Server\RequestHandlerInterface $handler ) : Psr\Http\Message\ResponseInterface { $session = $request->getAttribute( Zend\Expressive\Session\SessionMiddleware::SESSION_ATTRIBUTE ); // No Session data if (! $session->has(Zend\Expressive\Authentication\UserInterface::class)) { $user = ''; $roles = ['guest']; $request = $request->withAttribute( Zend\Expressive\Authentication\UserInterface::class, new Zend\Expressive\Authentication\DefaultUser( $user, $roles ) ); $response = $handler->handle($request); if ($request->getUri()->getPath() === '/login' || $response->getStatusCode() !== 403) { return $response; } return new Zend\Diactoros\Response\RedirectResponse('/login'); } // at /login page, redirect to authenticated page if ($request->getUri()->getPath() === '/login') { return new Zend\Diactoros\Response\RedirectResponse('/'); } // define roles from DB $sessionData = $session->get(Zend\Expressive\Authentication\UserInterface::class); $request = $request->withAttribute( Zend\Expressive\Authentication\UserInterface::class, new Zend\Expressive\Authentication\DefaultUser( $sessionData['username'], $sessionData['roles'] ) ); return $handler->handle($request); } }); $app->pipe(\Zend\Expressive\Authorization\AuthorizationMiddleware::class); $app->pipe(DispatchMiddleware::class);
When we logged as user, but want to access “admin” resource, eg: “/admin”, we will get “403 Forbidden” :
How about integration with “404” page?
With current setup, when we go to 404 page, eg: “/404”, we will get the “Resource ” not found”. To make it work with the NotFoundHandler, we need to inject NotFoundHandler to our authorization check. Let’s make above authorization check to a dedicated class with inject the NotFoundHandler, as follow:
<?php 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\Authentication\DefaultUser; use Zend\Expressive\Authentication\UserInterface; use Zend\Expressive\Handler\NotFoundHandler; use Zend\Expressive\Router\RouteResult; use Zend\Expressive\Session\SessionMiddleware; class AuthorizationMiddleware implements MiddlewareInterface { private $notFoundHandler; private $redirect; public function __construct(NotFoundHandler $notFoundHandler, string $redirect) { $this->notFoundHandler = $notFoundHandler; $this->redirect = $redirect; } public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { // 404 check early $routeResult = $request->getAttribute(RouteResult::class); if ($routeResult->isFailure()) { return $this->notFoundHandler->handle($request, $handler); } $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); // No Session data if (! $session->has(UserInterface::class)) { $user = ''; $roles = ['guest']; $request = $request->withAttribute( UserInterface::class, new DefaultUser( $user, $roles ) ); $response = $handler->handle($request); if ($request->getUri()->getPath() === $this->redirect || $response->getStatusCode() !== 403) { return $response; } return new RedirectResponse($this->redirect); } // at /login page, redirect to authenticated page if ($request->getUri()->getPath() === $this->redirect) { return new RedirectResponse('/'); } // define roles from DB $sessionData = $session->get(UserInterface::class); $request = $request->withAttribute( UserInterface::class, new DefaultUser( $sessionData['username'], $sessionData['roles'] ) ); return $handler->handle($request); } }
With above “$routeResult->isFailure()” check in line 30 above, we return 404 page early before check against authorization.
For service creation, we can create a AuthorizationMiddlewareFactory
for it:
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Zend\Expressive\Handler\NotFoundHandler; class AuthorizationMiddlewareFactory { public function __invoke(ContainerInterface $container) : MiddlewareInterface { $notFoundHandler = $container->get(NotFoundHandler::class); $redirect = $container->get('config')['authentication']['redirect']; return new AuthorizationMiddleware($notFoundHandler, $redirect); } }
Now, we can register as a service for the AuthorizationMiddleware
in the ConfigProvider class:
<?php class ConfigProvider { public function getDependencies() : array { return [ 'invokables' => [ /**/ ], 'factories' => [ // ... Middleware\AuthorizationMiddleware::class => Middleware\AuthorizationMiddlewareFactory::class, ], ]; } }
Now, in the pipeline, we have a cleaner, and with 404 handler integration:
$app->pipe(\Zend\Expressive\Session\SessionMiddleware::class); $app->pipe(RouteMiddleware::class); // ... $app->pipe(\App\Middleware\AuthorizationMiddleware::class); $app->pipe(\Zend\Expressive\Authorization\AuthorizationMiddleware::class); $app->pipe(DispatchMiddleware::class)
That’s it ;).
[…] How about authorization part? You can read my next post about create authorization functionality in zend expressive 3 […]
Hi,
How to solve the problem with ACL and 404 handler?
Hi Alex, you may handle 404 early before ACL check hit, btw, ZF Expressive 3 seems changed a lot in latest development, I may re-check after it got released.
After tried latest `zend-expressive-skeleton` 3.0.0.alpha3, you can actually inject the middleware that the duty is set “roles” ( on above code, on `new class implements Psr\Http\Server\MiddlewareInterface` ) with `\Zend\Expressive\Middleware\NotFoundMiddleware` with of course, a dedicated class and factory that inject the class with `\Zend\Expressive\Middleware\NotFoundMiddleware` service, so the middleware class structure can be like as follows:
I’ve updated the post with integration with 404 handler
[…] you followed my post about authentication and authorization posts with Expressive 3, this time, I write another session related post for securing request, […]
[…] you already followed my 4 previous expressive posts, all requirements already […]
What is generateUser function?
The new zend-expressive-authentication package drop UserTrait. The new method:
Thank you, I may revisit this post later.
I’ve updated the post with latest compatible components and its usage.
Can you write a blog on how to setup Ldap authentication in Zend Expressive 3?
I’ve never used LDAP yet
Thanks for the reply.
Do you have a githubpage of this tutorial?
I didn’t create a github repo for this. If you follow the step on the post, you should get a working example.
Cannot fetch middleware service “Zend\Expressive\Authorization\AuthorizationMiddleware”; service not registered, or does not resolve to an autoloadable class name
You said Now, in the pipeline, we have a cleaner, and with 404 handler integration. Is the Authorization Process part necessary? Because the Authorization Process is not there.
I also got this error Resource ‘login’ not found.
But it’s just in routes.php, before I started this tutorial I didn’t have that error yet.
I created a working example repository at https://github.com/samsonasik/mezzio-authentication-with-authorization which migrated to mezzio, follow the readme
What is the best way to do this: when I am logged in as an admin I want the admin to also be in the menu. just don’t know how to do that best. It should only be there when the admin is logged in.
By the way great tutorial!!
you can use view helper to display admin/user menu based on uri or session data
When it was an 404 page normally the 404.phtml file came on the screen.
But now I get this error when the page can’t find: Class ‘zend\Expressive\Session\Session’ not found.
(I deliberately use a page that does not exist, for example /test, which is not listed between the routers.)
Any idea to fix this?
the error shows the hint, the class not found. Check the spelling, and check whenever class exists.
If you didn’t follow the login part, I suggest you read it first https://samsonasik.wordpress.com/2018/01/12/create-login-functionality-in-expressive-3/ .
Anyway, expressive is now abandoned in favor of “mezzio”. I created a working example repository at https://github.com/samsonasik/mezzio-authentication-with-authorization