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 = :username'
        ],
        // ...
    ],

];

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: AdminPageAction:

<?php

declare(strict_types=1);

namespace App\Action;

use Interop\Http\Server\MiddlewareInterface;
use Interop\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\RedirectResponse;
use Zend\Expressive\Authentication\UserInterface;
use Zend\Expressive\Session\SessionMiddleware;
use Zend\Expressive\Template\TemplateRendererInterface;

class AdminPageAction implements MiddlewareInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ) : ResponseInterface
    {
        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
        if (! $session->has(UserInterface::class)) {
            return new RedirectResponse('/login');
        }

        return new HtmlResponse($this->template->render('app::admin-page', []));
    }
}

with factory as follows:

<?php

declare(strict_types=1);

namespace App\Action;

use Interop\Http\Server\MiddlewareInterface;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Template\TemplateRendererInterface;

class AdminPageFactory
{
    public function __invoke(ContainerInterface $container) : MiddlewareInterface
    {
        $template = $container->get(TemplateRendererInterface::class);
        return new AdminPageAction($template);
    }
}

We can register the AdminPageAction middleware at App\ConfigProvider::getDependencies():

<?php

class ConfigProvider
{
    public function getDependencies() : array
    {
        return [
            'invokables' => [ /**/ ],
            'factories'  => [
                // ...
                Action\AdminPageAction::class => Action\AdminPageFactory::class,
            ],
        ];
    }
}

Then let’s define route for it, eg: ‘/admin’:

// config/routes.php
$app->get('/admin', [
    App\Action\AdminPageAction::class,
], '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-acl:^1.0.0-dev

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 [‘authorization’] config, for example, at config/autoload/zend-expressive.global.php:

<?php
// config/autoload/zend-expressive.global.php

return [
    // ...
    'authorization' => [
        '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 after pipeRoutingMiddleware() and then pipe the Zend\Expressive\Authorization\AuthorizationMiddleware after it, as follow:

// config/pipeline.php
$app->pipeRoutingMiddleware();

$app->pipe(new class implements Interop\Http\Server\MiddlewareInterface{

    use Zend\Expressive\Authentication\UserRepository\UserTrait;

    public function process(
        Psr\Http\Message\ServerRequestInterface $request,
        Interop\Http\Server\RequestHandlerInterface $handler
    ) : Psr\Http\Message\ResponseInterface {
        $session = $request->getAttribute(
            Zend\Expressive\Session\SessionMiddleware::SESSION_ATTRIBUTE
        );

        // no session and at login page, set roles as "guest"
        if (! $session->has(Zend\Expressive\Authentication\UserInterface::class)) {
            if ($request->getUri()->getPath() === '/login') {
                $user = '';
                $roles = ['guest'];

                $request = $request->withAttribute(
                    Zend\Expressive\Authentication\UserInterface::class,
                    $this->generateUser(
                        $user,
                        $roles
                    )
                );
                return $handler->handle($request);
            }

            return new Zend\Diactoros\Response\RedirectResponse('/login');
        }

        // has session but 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,
            $this->generateUser(
                $sessionData['username'],
                $sessionData['roles']
            )
        );
        return $handler->handle($request);
    }
});

$app->pipe(\Zend\Expressive\Authorization\AuthorizationMiddleware::class);

By above, you can clean up $session->has() check in all pages.

Yes, you can move the middleware new class inside pipe() to dedicated class and register it as service to be called as its classname, use custom template, you name it.

When we logged as user, but want to access “admin” resource, eg: “/admin”, we will get “403 Forbidden” :

That’s it ;).

Advertisements

One Response

Subscribe to comments with RSS.

  1. […] How about authorization part? You can read my next post about create authorization functionality 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 )

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: