Cakephp 4 RequestAuthorizationMiddleware

I have implemented Authentication plugin and is working fine by itself. I am currently working on the Authorisation plugin. I am using the RequestAuthorizationMiddleware as per Request Authorization Middleware - 2.x

My middleware look like this:

public function middleware($middlewareQueue): \Cake\Http\MiddlewareQueue
{
	$middlewareQueue->add(new ErrorHandlerMiddleware(Configure::read('Error')))
	        		->add(new AssetMiddleware(['cacheTime' => Configure::read('Asset.cacheTime')]))
					->add(new RoutingMiddleware($this))
	            	->add(new BodyParserMiddleware())
	            	->add(new AuthorizationMiddleware($this, [
		                    'unauthorizedHandler' => [
		                        'className' => 'Authorization.Redirect',
		                        'url' => '/pages/permission',
		                        'exceptions' => [
		                            'MissingIdentityException' => 'Authorization\Exception\MissingIdentityException',
		                            'ForbiddenException' => 'Authorization\Exception\ForbiddenException',
		                        ]
		                    ]
	                	]))
	            	->add(new RequestAuthorizationMiddleware());

	return $middlewareQueue;
}


public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface {
    $mapResolver = new MapResolver();
    $mapResolver->map(ServerRequest::class, RequestPolicy::class);
    return new AuthorizationService($mapResolver);
}

RequestPolicy.php

namespace App\Policy;
use Authorization\Policy\RequestPolicyInterface;
use Cake\Http\ServerRequest;

class RequestPolicy implements RequestPolicyInterface
{
    /**
     * Method to check if the request can be accessed
     *
     * @param \Authorization\IdentityInterface|null $identity Identity
     * @param \Cake\Http\ServerRequest $request Server Request
     * @return bool
     */
    public function canAccess($identity, ServerRequest $request)
    {   
        if ($request->getParam('plugin') === 'DebugKit') {
            return true;
        }

        if(!empty($identity)){
            $userRole = $identity->role;
            $userStatus = $identity->status;
            $action = $request->getParam('action');
            $controller = $request->getParam('controller');

            switch ($controller) {
                case 'Users':
                    if($userRole === 'ADMIN') { 
                        return true; 
                    }
                    elseif($userRole == 'X' || $userRole == 'Y') {
                        if (in_array($action, ['logout','login', ....])) { //need to specify login and logout in this list even though it is declared in the skipAuthorization array in AppController; otherwise logged in users cannot logout.
                            return true;
                        }
                    }
                    break;
                    
                .....
                
            }
            return false;
        }else{
            
            return true; //works as expected only if it is return true
            // return false; //redirects to unauthorized redirect url (/pages/permission) causing redirect loop
        }
    }

Currently, authorization is working fine for the logged in users with the following issues:

  1. If identity is empty, when canAccess return false, redirects to unauthorized redirect url (/pages/permission) causing redirect loop. However, if I return true, authorization is working as expected.

I read RequestAuthorizationMiddleware would check authorization policies before Authentication kicks in. In that case, is it safe to return true from canAccess when there is no identity? or, is there any other way to solve this?

  1. In the AppController before filter, I have declared $this->Authorization->skipAuthorization(); for all the actions that do not need authentication. This working properly for the nonauthenticated users. However, If an identity exists and is valid, the actions in the skipAuthorization list is still chekcing authorization for the logged in users. So it redirects to unauthorized redirect url (/pages/permission) unless I specify that again in the canAccess method. Why do I need to declare that again even though it is declared in the skipAuthorization array in AppController;

Could anyone please point out what I am doing wrong? Any help is much appreciated.

Do you have in your pages controller this:

    public function beforeFilter(\Cake\Event\EventInterface $event)
    {
        parent::beforeFilter($event);
        // Configure the login action to not require authentication, preventing
        // the infinite redirect loop issue
        $this->Authentication->addUnauthenticatedActions(['display']);
    }

Why does “/pages/permission” cause a redirect loop? Is it not excluded from authentication?

@Zuluru @dirk Thanks for the pointers, redirect loop mentioned in my original post is resolved after excluding ‘display’ from authentication.

However, for nonauthenticated users when tying to access the functions that require authentication, if RequestPolicy/canAccess return false, are still redirected to unauthorized redirect url (pages/permission) instead of unauthenticatedRedirect url (users/login). If RequestPolicy/canAccess return true, everything is working as expected.

I tried to disable the Authorization and RequestAuthorization middlewares from the Application, then Authentication itself is working as expected for nonauthenticated users. i.e, Non authenticated users trying to access a function that requires authentication will be redirected to my unauthenticatedRedirect (users/login).

When I debug (eg. if I try to access Users/dashboard without logging in), the stack trace is as follows,
Application/middleware → Application/getAuthenticationService → Application/getAuthorizationService → RequestPolicy/canAccess → AppController/initialize → UsersController/beforeFilter → AppController/beforeFilter (IdentityCheck seems to be done after beforeFilter)

Is it safe to return true from RequestPolicy/canAccess function for nonauthenticated users, assuming the identitycheck will kick them back to the unauthenticatedRedirect url? Or is it because I’m doing something wrong? Could anybody please clarify? Thanks in advance.

Edit: I have missed to include the Authentication middleware in my original post. My middleware queue is with Authentication, Authorization and RequestAuthorization middlewares.

$middlewareQueue->...(other middlewares)
->add(new AuthenticationMiddleware($this))
->add(new AuthorizationMiddleware($this, [
                            'unauthorizedHandler' => [
                                'className' => 'Authorization.Redirect',
                                'url' => '/pages/permission',
                                'exceptions' => [
                                    'MissingIdentityException' => 'Authorization\Exception\MissingIdentityException',
                                    'ForbiddenException' => 'Authorization\Exception\ForbiddenException',
                                ]
                            ]
                        ]))
->add(new RequestAuthorizationMiddleware());

I have my $this->Authentication->allowUnauthenticated([‘login’, ‘logout’, ‘display’…]) in my AppController/initialize.

Im having the same problem. If identity is empty and I return false I get an authorization error, if i return true it will redirect to login page.

@pavia20 I managed to resolve the issue by returning false when identity is empty, and then use a CustomRedirectHandler middleware to redirect the users according to various conditions.

Thanks - that worked! Although, I’m now wondering why the exception thrown is a ForbiddenException rather than a MissingIdentityException. Nevertheless the custom redirect seems to work, but if you have any idea why it works, that would be great :slight_smile: