Welcome to Abdul Malik Ikhsan's Blog

Create Authorization functionality in Expressive 3

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

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 ;).

14 Responses

Subscribe to comments with RSS.

  1. […] How about authorization part? You can read my next post about create authorization functionality in zend expressive 3 […]

  2. Alex said, on January 29, 2018 at 7:31 pm

    Hi,
    How to solve the problem with ACL and 404 handler?

    • samsonasik said, on February 7, 2018 at 1:02 am

      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.

    • samsonasik said, on February 9, 2018 at 3:20 am

      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:

      class AuthorizeHandler implements \Psr\Http\Server\MiddlewareInterface{
      { // ...
          private $notFoundMiddleware;
      
          public function __construct(\Zend\Expressive\Middleware\NotFoundMiddleware $notFoundMiddleware)
          {
              $this->notFoundMiddleware = $notFoundMiddleware;
          }
      
          public function process(
              \Psr\Http\Message\ServerRequestInterface $request,
              \Psr\Http\Server\RequestHandlerInterface $handler
          ) : \Psr\Http\Message\ResponseInterface {
      
              $routeResult = $request->getAttribute(\Zend\Expressive\Router\RouteResult::class, false);
              if (! $routeResult) {
                  return $this->notFoundMiddleware->process($request, $handler);
              }
             
              // ... 
         }
      }
      
    • samsonasik said, on December 26, 2018 at 10:48 pm

      I’ve updated the post with integration with 404 handler

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

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

  5. icetee said, on November 19, 2018 at 7:09 pm

    What is generateUser function?

    • icetee said, on November 19, 2018 at 7:34 pm

      The new zend-expressive-authentication package drop UserTrait. The new method:

            new \Zend\Expressive\Authentication\DefaultUser(
              $sessionData['username'],
              $sessionData['roles']
            );
      
      • samsonasik said, on November 19, 2018 at 8:00 pm

        Thank you, I may revisit this post later.

      • samsonasik said, on December 26, 2018 at 7:00 pm

        I’ve updated the post with latest compatible components and its usage.

  6. Toral said, on January 15, 2019 at 4:46 am

    Can you write a blog on how to setup Ldap authentication in Zend Expressive 3?


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: