RFC: authorization module for Expressive and PSR-7

Update: I created a diagram to show the usage of RFC authentication and RFC authorization.

Goal

The goal of this RFC is to propose an authorization middleware for Expressive and PSR-7 applications.

This authorization module will be used to authorize authenticated users based on URL route and HTTP methods.

Requirements

This authorization module will require an authentication module to get a user’s role to be passed as PSR-7 attribute. This user attribute will be the input of the authorization middleware.

The authentication module will create a PSR-7 attribute with name AuthorizationInterface::class containing the user’s role. Below is reported an example in pseudo-code.

use Zend\Expressive\Authorization\AuthorizationInterface;

class AuthenticationMiddleware implements ServerMiddlewareInterface
{
    public function process($request, $delegate)
    {
        if (/* not authenticated */)
            return new RedirectResponse($this->config['redirect']);
        }
        $role = /* get the user's role */
        return $delegate->process(
            $request->withAttribute(AuthorizationInterface::class, $role)
        );
    }
}

The user’s role stored in the PSR-7 attribute will be used by the Authorization middleware for permission grants, as follows:

$role = $request->getAttribute(AuthorizationInterface::class, false);
if (false === $role) {
    return new Zend\Diactoros\Response\EmptyResponse(401);
}

If the user will be not available, the authorization module will return a 401 Unauthorized response.

The Zend\Expressive\Authorization\AuthorizationInterface interface is reported as follows:

namespace Zend\Expressive\Authorization;

use Psr\Http\Message\ServerRequestInterface;

interface AuthorizationInterface
{
    /**
     * Check if a role is granted for the request
     *
     * @param string $role
     * @param ServerRequestInterface $request
     * @return bool
     */
    public function isGranted(string $role, ServerRequestInterface $request): bool;
}

Implementation

The idea is to build an Expressive module that offers authorization using specific adapters (e.g. using zend-permissions-acl, zend-permissions-rbac, etc).

For instance, the ZendRbac adapter will use the zend-permissions-rbac library for implementing the RBAC system.
Using the RBAC terminology, a role is the user’s role and a resource is the route name. In this way we can authorize URLs and HTTP methods for a specific user’s role.

This approach is the same used in this blog post. The authorization can be configured using a PHP file like to the follows:

return [
   'authorization' => [
        'roles' => [
            'administrator' => [],
            'editor'        => ['administrator'],
            'contributor'   => ['editor'],
        ],
        'permissions' => [
            'contributor' => [
                'admin.dashboard',
                'admin.posts',
            ],
            'editor' => [
                'admin.publish',
            ],
            'administrator' => [
                'admin.settings',
            ],
        ],
    ],
];

where the roles can be configured using a parent role (e.g. administrator is parent of editor). The permissions are configured using route names (e.g. contributor is allowed to access the routes admin.dashboard and admin.posts).

The authorization middleware will be configured in the pipe of a route. Here an example:

$app->get('/admin/dashboard', [
    AuthenticationMiddleware::class,
    AuthorizationMiddleware::class,
    Action\DashboardAction::class
], 'admin.dashboard');

Before the authorization, we assumed the use of an authentication module (AuthenticationMiddleware). The AuthenticationMiddleware will store a user’s role as PSR-7 attribute.

Prototype

A prototype of the implementation will be available at ezimuel/zend-expressive-authorization.

1 Like

@enrico I was a touch confused the first read through about

$request->getAttribute(AuthenticationMiddleware::class, false);

it may help to have a small snippet where you show that the AuthenticationMiddleware is injecting a user into the $request with $request->withAttribute(self::class, $user); or something similar. Just a thought.

@moderndeveloperllc I just added an AuthenticationMiddleware class as example to show how to inject the user’s attribute in the PSR-7 request. I hope this will clarify the RFC.

1 Like

Is there a specific reason why you only want to support RBAC? It would be very nice to have ACL support as well.

Moreover, for the purpose of the authorization module, we will need the user’s role. We can get the role using a $user->getRole() function, a $user->role property or a $user[‘role’] value. The choose of which method to use is left to the user implementation.

We could state that there should be some middleware before the execution of the Authorization middleware that adds the users roles to the request. In that way we can keep all complex logic out of this middleware.

$role = $request->getAttribute(AuthorizationMiddleware::USER_ROLE, false)

Hey @enrico, you beat me to it. I was going to propose something similar after some recent experiences actually. I’d love to work on this with you. I like the point made by @jaapio actually. Would there be the possibility of having an either/or situation where the developer could choose between RBAC and ACL?

@jaapio and @settermjd I like the idea to inject in the request the user’s role as an attribute, this will simplify the implementation.

Regarding the usage of RBAC this is just an implementation detail. My idea is to offer an authorization middleware to allow or deny a route based on user’s role. Moreover, using RBAC we have also the possibility to use dynamic assertions.

If we want to support also ACL we need to provide a way to use both. We can implement an AuthorizationInterface with some specific adapters for zend-permissions-acl and zend-permissions-rbac. But we really need it?

P.S. I updated the RFC using the USER_ROLE attribute.

1 Like

@jaapio and @settermjd I just pushed a work in progress code in ezimuel/zend-expressive-authorization.

2 Likes

I like the idea of an AuthorizationInterface and adapters. I think there will be a lot of users that are using ACL and willing to switch to a PSR-7 implementation. There are more download on packagist for ACL then there are for RBAC.

Beside that introducing an AuthorizationInterface might even make the adaption of this package easier since it disconnects the RFC from the actual zend packages.

I think it doesn’t introduce much more complexity to the RFC.

Hello, @enrico ! It’s a really good idea but why use zend-permissions-rbaczend-permission-rbac that it’s a old implementation instead of zfc-rbac. This implementation allow us to choose between role from configuration or database.

Hey @enrico, did you take a look at Helios yet? It’s pretty much doing exactly that for authentication :slight_smile:

2 Likes

Hi @Orkin, even if zend-permissions-rbac is an old implementation is quite stable and enough for the purpose of an authorization module. That said, I’m rewriting the module to use authorization adapters. In this way, we can use zfc-rbac or whatever we want.

1 Like

Hi @DASPRiD, the Helios project looks very good but it’s more an authentication middleware. The goal of zend-expressive-authorization is to provide only an authorization module, starting from a user’s role provided by an authentication module. We can use Helios as authentication module, I’ll look into it for more details. Thanks!

I just updated the zend-expressive-authorization code base with the usage of authorization adapters using an AuthorizationInterface interface.

Each adapter is stored in the Adapter folder. Right now, I only implemented the ZendRbac adapter for zend-permissions-rbac.

Thanks to your feedbacks, the code base of the AuthorizationMiddleware is much simpler now with an extensible and open architecture.

2 Likes

Authorizing multiple routes with admin.users.*, would that be possible? Or is that something that needs to be added to the rbac? I haven’t used zend-permissions-rbac yet.

@xtreamwayz this is something specific to the authorization logic. With the new design using adapter you can support it using a custom adapter.
Right now, the RBAC adapter is implemented here and it does not support wildcard like * in the route name.
Do you think that this feature should be included in the RBAC default adapter?

Hi @enrico is there any reason you use the practice where you put Identity object in request and later pull it from the request?

// Authentication module
return $delegate->process($request->withAttribute(<role-attribute-name>, $role));

// Authorization module
$role = $request->getAttribute($this->authorization->getRoleAttributeName(), false);

Instead of injecting the Identity object in Authentication and Authorization module too.
In authentication module you can fill out the Identity with user data (and role too) and later in Authorisation module you will have the role info but Identity will be injected in constructor.

I now now it’s only a string but as I can see in tutorial there is same practice with Identity object.

HI @tasmaniski the idea is to develop a general purpose Authorization module based on PSR-7 middleware. That means we can only pass data by PSR-7 attributes in the HTTP request. For the Authorization module, I’m assuming a user’s role to be passed from a third-party Authentication middleware. For that reason, I cannot inject the user’s role in the constructor of Authorization module.

In the example presented in the blog post I assumed to implement the Authorization and Authentication module in the same application. In that case, you can use a different approach, as you suggested.

The identity is something derived from the request, and, as such cannot and will not be available to factories. We may have the forthcoming Authentication module define an identity instance, but the least common denominator is to provide a string role name.

@enrico Ok I see now, then it looks quite regular to pass the identity. If I make an ZF/ZE app with all zend components I would probably do it on second approach…

@matthew

The identity is something derived from the request, and, as such cannot and will not be available to factories.

This is really interesting.
Thanks.

I just updated the https://github.com/ezimuel/zend-expressive-authorization repository with the ACL adapter implementation. Right now the adapters are in the same repository. The idea is to separate in external components, e.g. zend-expressive-authorization-acl as we did for zend-expressive-router.

I also simplified the usage of the role PSR-7 attribute using the name AuthorizationInterface::class. I removed the getRoleAttributeName() in AuthorizationInterface.