Create isGranted View Helper for Mezzio Application with mezzio-authorization-acl component
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
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!
- Setup Apigility Authentication with Oauth2
- 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', ], ], ];
- Define our own
NamedOAuth2ServerFactory
to use our ownOAuth2ServerFactory
forOAuth2\Server
instance creationnamespace 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 ); } }; } }
- 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() } }
- As we want custom PdoAdapter, we need to map
\ZF\OAuth2\Adapter\PdoAdapter::class
to ourPdoAdapter
, 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']); } }
- 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' => [], ], []); } }
- 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, ], ], // ... ];
-
Time to attach the
\ZF\OAuth2\Adapter\PdoAdapter
into our delegated serviceZF\MvcAuth\Authentication\DefaultAuthenticationListener
via delegator factorynamespace 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; } }
-
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 😉
leave a comment