We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Implement middleware to secure some routes

Hi all, I'm in the process of learning Phalcon and I've come across a roadblock. I've spent 9 hours in total this week researching this and I've made 0 progress so I decided to ask here.

Currently in my application (not micro), I have hard-coded checks in my controllers to see if a user has logged in from session. I want to make use of middleware to 'wrap' certain routes to require a user to be logged in to access, but I'm really struggling finding an implementation that works.

So far I've tried using a SecurityPlugin found in INVO, following this tutorial: https://php-phalcon-docs.readthedocs.io/en/latest/reference/tutorial-invo-2.html. My current code is identical to what's shown in the tutorial. This doesn't work for seemingly no reason as I can still access private resources when logged out/no session.

Does anyone have any useful resources I can look into? The Phalcon docs leaves me with more questions than answers in regards to Dispatcher, Event Manager, and Micro. I'm starting to get tired seeing the same links when searching for answers.

For context I created the project using Phalcon DevTools (4.0.3) running on Phalcon version 4.0.6. Fyi I can't clone Vokura to modify or use composer modules made by other people.

The following code works, so I know it's not an issue with attaching a plugin to the event manager.

use Phalcon\Events\Event;
use Phalcon\Mvc\Dispatcher;

class TestPlugin extends Injectable {

    public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) {
        $auth = $this->session->get('AUTH_ID');
        if(!$auth) {
            $this->flashSession->error("You are not authenticated");
        } else {
            $this->flashSession->error("You are authenticated");
        }
    }
}

In services.php:

$di->set('dispatcher', function() use ($di) {

    $eventsManager = $di->getShared("eventsManager");
    $eventsManager->attach('dispatch:beforeExecuteRoute', new TestPlugin);

    $dispatcher = new Dispatcher;
    $dispatcher->setEventsManager($eventsManager);

    return $dispatcher;
});
edited Jul '20

Hi, you are looking at very old docs.

You can clone latest Vokuro or Invo repositories and see example implementations.

In case you have many questions, you can ask them in our Discord...

edited Jul '20

Your code isn't far off. Like has been said, use the beforeDispatch event rather than beforeExecuteRoute. They can both be used to prevent unauthorized access, but beforeDispatch is executed before the Controller is initialized, so it's a little lighter weight.

Your code isn't having the intended effect because you're not returning false if there is no session. In my setup I have 2 components - an Auth component that checks if the user is logged in (ie: authentication), and an ACL component that checks if the user is allowed to view the current route (ie: authorization).

If the user isn't authenticated, I just redirect them back to the login page. If the user isn't authorized, I forward them to a special, open-to-all page that says they're not allowed:

header('HTTP/1.1 401 Unauthorized');
$Dispatcher->forward(['namespace'  => 'Controller',
                      'controller' => 'index',
                      'action'     => 'notAllowed']);
return FALSE;

Also, as an aside, if you surround your code blocks with three backticks and a language name, you'll get syntax highlighting (example here uses single quotes)

'''php

//code here

'''

Okay I've changed the method to beforeDispatch and returned FALSE if the current role is not allowed. I also rewrote the beforeDispatch and getAcl methods from the INVO tutorial for Phalcon 4.0. I can still access private components as a guest however:

    $allowed = $acl->isAllowed($role, $controller, $action);
    if($allowed === true) {
        // Allowed is always true no matter what role I am
        $this->flashSession->error("Allowed");
    } else {
        $this->flashSession->error("Not Allowed");
    }

Here's my getAcl() method:


private function getAcl() {
        // Initialise a new ACL list using the Memory adapter
        $acl = new AclList();

        // Set default action
        $acl->setDefaultAction(Enum::DENY);

        // Define user roles
        $roles = [
            'users' => new Role('Users'),
            'guests' => new Role('Guests'),
            'admins' => new Role('Admins')
        ];
        // Map roles in ACL
        foreach($roles as $role) {
            $acl->addRole($role);
        }

        // Define private components that only logged in users can access (users/admins)
        $privateComponents = [
            'admin' => [
                'index',
                'update',
                'delete'
            ]
        ];
        // Map private components to ACL
        foreach($privateComponents as $componentName => $actions) {
            $acl->addComponent(
                new Component($componentName),
                $actions
            );
        }

        // Define private components that only admins can access

        // Define public components that all users can access
        $publicComponents = [
            'index' => [
                'index',
                'archive',
                'view'
            ],
            'admin' => [
                'login',
                'signup'
            ],
            'user' => [
                'login',
                'signup',
                'logout'
            ]
        ];
        // Map public components to ACL
        foreach($publicComponents as $componentName => $actions) {
            $acl->addComponent(
                new Component($componentName),
                $actions
            );
        }

        // Link public components to allow for all roles
        foreach($roles as $role) {
            foreach($publicComponents as $resource => $actions) {
                $acl->allow(
                    $role->getName(),
                    $resource,
                    '*'
                );
            }
        }
        // Link private components to allow for users and admins role
        foreach($privateComponents as $resource => $actions) {
            foreach($actions as $action) {
                $acl->allow(
                    'Users',
                    $resource,
                    $action
                );
                $acl->allow(
                    'Admins',
                    $resource,
                    $action
                );
            }
        }
        return $acl;
    }


455
Accepted
answer

Wait I think I solved it. Specifiying 'Guests' to be denied fixed it:


// Link private components to allow for users and admins role
        foreach($privateComponents as $resource => $actions) {
            foreach($actions as $action) {
                $acl->allow(
                    'Users',
                    $resource,
                    $action
                );
                $acl->allow(
                    'Admins',
                    $resource,
                    $action
                );
                // I needed to add this code:
                $acl->deny(
                    'Guests',
                    $resource,
                    $action
                );
            }
        }

Which means the $acl->setDefaultAction(Enum::DENY); doesn't work. Any ideas why?

edited Jul '20

Where you add your public components - you're setting * as the action, not $actions. So anything within that component name (ie: controller) will be accessible to the public. Changing that to $actions should fix it.

Enum::DENY is the default mode, so you shouldn't need to specify it.

I think you can simplify/DRY this quite a bit by changing how you define your components and using wildcards:

<?php

private function getAcl() {
    // Initialise a new ACL list using the Memory adapter
    $acl = new AclList();

    // It might actually be useful to define these as ROLE_GUEST, ROLE_USER, ROLE_ADMIN
    // constants in your app, in case other components need to use them.
    foreach(['Guests','Users','Admins'] as $role) {
        $acl->addRole($role);
    }

    // Define all components, specify explicitly which roles have access to which components
    $components = [
        'admin'=>[
            'archive'=>['Users','Admins'],
            'index'  =>['Users','Admins'],
            'view'   =>['Users','Admins']
        ],
        'index'=>[
            '*'=>['*']
        ],
        'admin'=>[
            'login' =>['*'],
            'signup'=>['*']
        ],
        'user'=>[
            'login'  =>['*'],
            'signup' =>['*'],
            'logout' =>['*']
        ]
    ]

    // Add all components
    foreach($components as $componentName => $actions) {
        $acl->addComponent(
            new Component($componentName),
            array_keys($actions);
        );

        foreach($actions as $action=>$roles){
            foreach($roles as $role){
                $acl->allow($role,$componentName,$action);
            }
        }
    }
    return $acl;
}

Ah I see, that's pretty silly of me.

I used your code and everything seems to be working as expected. Thank you so much!

That's why pair-programming is a thing ;)

I hadn't tested my code at all - glad to see it worked.