Welcome to Abdul Malik Ikhsan's Blog

Create isGranted View Helper for Mezzio Application with mezzio-authorization-acl component

Posted in Mezzio by samsonasik on February 15, 2020

This post is inspired by @KiwiJuicer question at Laminas’s Slack, about how to verify granted access on resource in the layout or view part, eg:

<?php if ($this->isGranted('admin.add')) : ?>
    <button name="add">Add</button>
<?php endif; ?>

OR

<?php if ($this->isGranted('admin.edit', ['id' => 1])) : ?>
    <button name="edit">Edit</button>
<?php endif; ?>

We can use Mezzio\Authorization\LaminasAcl service, but it requires a Psr\Http\Message\ServerRequestInterface instance to be passed at 2nd parameter as signature:

public function isGranted(string $role, ServerRequestInterface $request) : bool
{

}

We can pull the role from session, but how about Psr\Http\Message\ServerRequestInterface instance? Well, we don’t need middleware to fill that! We can create a RouteResult from Mezzio\Router\LaminasRouter service with utilize Mezzio\LaminasView\UrlHelper to get path from route name as resource name, with use Laminas\Diactoros\ServerRequestFactory to create Laminas\Diactoros\ServerRequest instance, so, the view helper can be as follow:

<?php

// src/App/View/Helper/IsGranted.php

declare(strict_types=1);

namespace App\View\Helper;

use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use Laminas\View\Helper\AbstractHelper;
use Mezzio\Authorization\Acl\LaminasAcl;
use Mezzio\LaminasView\UrlHelper;
use Mezzio\Router\LaminasRouter;
use Mezzio\Router\RouteResult;

class IsGranted extends AbstractHelper
{
    private $acl;
    private $getRole;
    private $url;
    private $router;

    public function __construct(
        LaminasAcl    $acl,
        GetRole       $getRole, 
        UrlHelper     $url, 
        LaminasRouter $router
    ) {
        $this->acl     = $acl;
        $this->getRole = $getRole;
        $this->url     = $url;
        $this->router  = $router;
    }

    public function __invoke(
        string $resource,
        array $routeParams = [], 
        array $queryParams = []
    ): bool {
        $request = ServerRequestFactory::fromGlobals();
        $request = $request->withUri(
            new Uri(($this->url)($resource, $routeParams, $queryParams))
        );

        $request = $request->withAttribute(
            RouteResult::class,
            $this->router->match($request)
        );

        return $this->acl->isGranted(($this->getRole)(), $request);
    }
}

Above, I assume that you already have another view helper or service to get role, eg, named App\View\Helper\GetRole so the factory for the isGranted view helper can be as follow:

<?php

// src/App/View/Helper/IsGrantedFactory.php

declare(strict_types=1);

namespace App\View\Helper;

use Laminas\View\HelperPluginManager;
use Mezzio\Authorization\Acl\LaminasAcl;
use Mezzio\Router\LaminasRouter;
use Psr\Container\ContainerInterface;

class IsGrantedFactory
{
    public function __invoke(ContainerInterface $container): IsGranted
    {
        $acl                 = $container->get(LaminasAcl::class);
        $helperPluginManager = $container->get(HelperPluginManager::class);
        $getRole             = $helperPluginManager->get('getRole');
        $url                 = $helperPluginManager->get('url');
        $router              = $container->get(LaminasRouter::class);

        return new IsGranted($acl, $getRole, $url, $router);
    }
}

We can register the view helper to App\ConfigProvider class:

<?php

// src/App/ConfigProvider.php

declare(strict_types=1);

namespace App;

class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'dependencies' => $this->getDependencies(),
            'templates'    => $this->getTemplates(),
            'view_helpers' => [
                'invokables' => [
                    'getRole' => View\Helper\GetRole::class,
                ],
                'factories'  => [
                    'isGranted' => View\Helper\IsGrantedFactory::class,
                ],
            ],
        ];
    }

    public function getDependencies(): array
    { /* */ }

    public function getTemplates(): array
    { /* */ }
}

Now, we can check use “$this->isGranted($resource)” or “$this->isGranted($resource, $routeParams, $queryParams)” check in the layout or view.

This source code example can be found at samsonasik/mezzio-authentication-with-authorization repository that you can try yourself 😉

Apigility: Create custom Authentication for Oauth2 with service delegators

Posted in Tutorial PHP, Zend Framework by samsonasik on August 21, 2016

apigility logo Custom authentication in apigility is do-able with service delegators. We need to wrap ZF\MvcAuth\Authentication\DefaultAuthenticationListener::class in decorator. For example, we want to use ZF\OAuth2\Adapter\PdoAdapter but want to modify checkUserCredentials($username, $password) to include is_active check. Let’s do it!

  1. Setup Apigility Authentication with Oauth2
  2. With in assumption, we have the following config:
    return [
    // ... config/autoload/local.php
        'zf-oauth2' => [
          'db' => [
              'driver' => 'PDO_Mysql',
              'username' => 'root',
              'password' => '',
              'dsn' => 'mysql:host=localhost;dbname=app_oauth',
          ],
        ],
    // ...
    ];
    

    We can then modify config/autoload/zf-mvc-auth-oauth2-override.global.php as follows:

    // config/autoload/zf-mvc-auth-oauth2-override.global.php
    return [
        'service_manager' => [
            'factories' => [
                'ZF\OAuth2\Service\OAuth2Server' 
                    => 'Application\MvcAuth\NamedOAuth2ServerFactory',
            ],
        ],
    ];
    
  3. Define our own NamedOAuth2ServerFactory to use our own OAuth2ServerFactory for OAuth2\Server instance creation
    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    
    class NamedOAuth2ServerFactory
    {
        /**
         * @param ContainerInterface $container
         *
         * @return callable
         */
        public function __invoke(ContainerInterface $container)
        {
            $config = $container->get('config');
    
            $mvcAuthConfig = isset($config['zf-mvc-auth']['authentication']['adapters'])
                ? $config['zf-mvc-auth']['authentication']['adapters']
                : [];
    
            $servers = (object) ['application' => null, 'api' => []];
    
            return function ($type = null) use (
               $mvcAuthConfig, $container, $servers
            ) {
                foreach ($mvcAuthConfig as $name => $adapterConfig) {
                    if (!isset($adapterConfig['storage']['route'])) {
                        // Not a zf-oauth2 config
                        continue;
                    }
    
                    if ($type !== $adapterConfig['storage']['route']) {
                        continue;
                    }
    
                    // Found!
                    return $servers->api[$type] = OAuth2ServerFactory::factory(
                        $adapterConfig['storage'],
                        $container
                    );
                }
            };
        }
    }
    
  4. Create our Application\MvcAuth\OAuth2ServerFactory based on \ZF\MvcAuth\Factory\OAuth2ServerFactory
    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    use OAuth2\GrantType\AuthorizationCode;
    use OAuth2\GrantType\ClientCredentials;
    use OAuth2\GrantType\RefreshToken;
    use OAuth2\GrantType\UserCredentials;
    use OAuth2\GrantType\JwtBearer;
    use OAuth2\Server as OAuth2Server;
    
    final class OAuth2ServerFactory
    {
        private function __construct()
        {
        }
    
        public static function factory(array $config, ContainerInterface $container)
        {
            $allConfig = $container->get('config');
            $oauth2Config = isset($allConfig['zf-oauth2']) ? $allConfig['zf-oauth2'] : [];
            $options = self::marshalOptions($oauth2Config);
    
            $oauth2Server = new OAuth2Server(
                $container->get(\ZF\OAuth2\Adapter\PdoAdapter::class),
                $options
            );
    
            return self::injectGrantTypes($oauth2Server, $oauth2Config['grant_types'], $options);
        }
    
       private static function marshalOptions(array $config)
       { 
           // same as \ZF\MvcAuth\Factory\OAuth2ServerFactory::marshalOptions()
       }
       
        private static function injectGrantTypes(
           OAuth2Server $server,
           array $availableGrantTypes,
           array $options
       ) {
          // same as \ZF\MvcAuth\Factory\OAuth2ServerFactory::injectGrantTypes()
       }
    }
    
  5. As we want custom PdoAdapter, we need to map \ZF\OAuth2\Adapter\PdoAdapter::class to our PdoAdapter, for example: Application\MvcAuth\PdoAdapter:
    namespace Application\MvcAuth;
    
    use Zend\Crypt\Bcrypt;
    use ZF\OAuth2\Adapter\PdoAdapter as BasePdoAdapter;
    
    class PdoAdapter extends BasePdoAdapter
    {
        public function checkUserCredentials($username, $password)
        {
            $stmt = $this->db->prepare(
                'SELECT * from oauth_users where username = :username and is_active = 1'
            );
            $stmt->execute(compact('username'));
            $result = $stmt->fetch();
    
            if ($result === false) {
                return false;
            }
    
            // bcrypt verify
            return $this->verifyHash($password, $result['password']);
        }
    }
    
  6. For our Application\MvcAuth\PdoAdapter, we need to define factory for it:
    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    use ZF\OAuth2\Factory\PdoAdapterFactory as BasePdoAdapterFactory;
    
    class PdoAdapterFactory extends BasePdoAdapterFactory
    {
        public function __invoke(ContainerInterface $container)
        {
            $config = $container->get('config');
    
            return new PdoAdapter([
                'dsn' => $config['zf-oauth2']['db']['dsn'],
                'username' => $config['zf-oauth2']['db']['username'],
                'password' => $config['zf-oauth2']['db']['password'],
                'options' => [],
            ], []);
        }
    }
    
  7. Register the adapter into service manager into config/autoload/global.php
    // config/autoload/global.php
    return [
    // ... 
        'service_manager' => [
            'factories' => [
                \ZF\OAuth2\Adapter\PdoAdapter::class => 
                     \Application\MvcAuth\PdoAdapterFactory::class,
            ],
        ],
    // ...
    ];
    

  8. Time to attach the \ZF\OAuth2\Adapter\PdoAdapter into our delegated service ZF\MvcAuth\Authentication\DefaultAuthenticationListener via delegator factory

    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    use OAuth2\Server as OAuth2Server;
    use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
    use ZF\MvcAuth\Authentication\OAuth2Adapter;
    
    class AuthenticationListenerDelegatorFactory implements DelegatorFactoryInterface
    {
        public function __invoke(
           ContainerInterface $container,
           $name,
           callable $callback,
           array $options = null
       ) {
            $listener = call_user_func($callback);
            $listener->attach(
                new OAuth2Adapter(
                    new Oauth2Server(
                        $container->get(\ZF\OAuth2\Adapter\PdoAdapter::class),
                        ['Bearer']
                    )
                )
            );
    
            return $listener;
        }
    }
    

  9. Last one! Register our AuthenticationListenerDelegatorFactory into service delegators:

    // config/autoload/global.php
    return [
    // ... 
        'service_manager' => [
            'delegators' => [
                \ZF\MvcAuth\Authentication\DefaultAuthenticationListener::class => [
                    \Application\MvcAuth\AuthenticationListenerDelegatorFactory::class
                ],
            ],
        ],
    // ...
    ];
    

Done 😉