Skip to content

Feature Request: Add allowUnauthorized() to AuthorizationComponent for parity with Authentication plugin #339

Open

Description

@alphp

Description

Currently, the Authentication plugin provides a very convenient $this->Authentication->allowUnauthenticated(['action']) method to skip authentication checks for specific controller actions.

However, the Authorization plugin lacks a direct equivalent, forcing developers to manually call $this->Authorization->skipAuthorization() inside every public action or build custom logic in beforeFilter.

I propose adding an allowUnauthorized() method to AuthorizationComponent to bring API parity between both core plugins and streamline the handling of public/unauthorized actions.

Proposed Solution

We can introduce an internal tracker for unauthorized actions within the component and intercept the authorization checks before they hit the policies.

Here is a working implementation I am currently using by extending the base component:

<?php
declare(strict_types=1);

namespace Authorization\Controller\Component;

use Authorization\Policy\ResultInterface;
use Cake\Controller\Component;

class AuthorizationComponent extends Component 
{
    protected array $unauthorizedActions = [];

    /**
     * Allow specific actions to bypass authorization checks.
     *
     * @param array<string> $actions List of controller actions.
     * @return $this
     */
    public function allowUnauthorized(array $actions): static 
    {
        $this->unauthorizedActions = $actions;

        return $this;
    }

    /**
     * Overriding performCheck to automatically skip authorization for allowed actions
     */
    protected function performCheck(mixed $resource, ?string $action = null, string $method = 'can'): ResultInterface|bool 
    {
        $request = $this->getController()->getRequest();

        if ($action === null) {
             $action = $this->getDefaultAction($request);
        }

        if (in_array($action, ($this->unauthorizedActions)) {
            $this->skipAuthorization();
            return true;
        }

        return parent::performCheck($resource, $action, $method);
    }
}

Example Usage

This allows a much cleaner and intuitive setup in any controller's beforeFilter:

public function beforeFilter(\Cake\Event\EventInterface $event)
{
    parent::beforeFilter($event);
    $this->Authorization->allowUnauthorized([
        'login',
        'logout',
        'verify',
    ]);

    // Global authorization check for the rest of the actions
    if (!$this->Authorization->can($this)) {
        return $this->redirect('/');
    }
}

Why this should be added

  1. API Consistency: Alignment with how AuthenticationComponent::allowUnauthenticated() works.
  2. Cleaner Controllers: Avoids cluttering public actions with repetitive $this->Authorization->skipAuthorization() calls.
  3. Better DX: Centralizes access control rules in beforeFilter right next to authentication rules.

Additional Notes

  • Naming Conventions: I am completely open to suggestions regarding the names of the allowUnauthorized() method and the $unauthorizedActions property if the core team prefers a different naming convention (e.g., allowBypass(), skipActions(), etc.).
  • Pull Request: If the core team values this feature and approves the overall approach, I am more than happy to create and submit the Pull Request along with the necessary test cases.

Activity

  1. dereuromark commented on Aug 28, 2026

    @dereuromark
    Member

    AuthorizationComponent has a skipAuthorization config key and an authorizeAction() handler bound to Controller.startup:

    $this->loadComponent('Authorization.Authorization', [
        'skipAuthorization' => ['login'],
    ]);

    Real gap = no fluent setter mirroring authorizeModel(string ...$actions) maybe. But it is 6 lines so far.

  2. alphp commented on Aug 29, 2026

    @alphp
    Author

    Thank you for the quick feedback!

    You are completely right about the skipAuthorization config key. However, the limitation of configuring it inside $this->loadComponent() (usually in AppController::initialize()) is that it forces you to centralize all skipped actions of the entire application in one place, or override the component configuration per-controller in a less intuitive way.

    My main goal is modularity and keeping controller-specific logic inside the controller itself. AppController shouldn't need to know that UsersController has a verifyEmail action, or that BillingController has a webhook action that bypasses authorization.

    To test the current limitations, I tried dynamically modifying the configuration inside UsersController::beforeFilter() right before calling manual checks:

    $this->Authorization->setConfig('skipAuthorization', ['login', 'logout']);
    
    if (!$this->Authorization->can($this)) {
        return $this->redirect('/');
    }

    As expected, this does not work. Because skipAuthorization is tightly bound to the Controller.startup event handler, modifying the config array at runtime inside beforeFilter is ignored or doesn't alter the internal state in time for manual $this->Authorization->can() structural checks.

    Furthermore, there is a fundamental difference in scope regarding your mention of authorizeModel(): that method is designed to map actions to ORM Model/Entity policies. In my case, I am authorizing Controller actions/methods themselves dynamically at the routing level. Since this approach controls the request flow before hitting any model repository, a dedicated controller-level mechanic like allowUnauthorized() is architecturally the correct place for it.

    If you agree that adding a fluent setter to dynamically append actions to skipAuthorization improves the DX and architecture, I'd be happy to work on a PR for it!

  3. dereuromark commented on Aug 29, 2026

    @dereuromark
    Member

    What do u think about my PR?

  4. alphp commented on Aug 29, 2026

    @alphp
    Author

    My apologies! I completely missed that PR, thank you for pointing it out.

    I just pulled your branch and tested your proposal using the dynamic approach in a controller's beforeFilter:

    // Inside UsersController::beforeFilter()
    $this->Authorization->skipAuthorizationActions(['login', 'logout']);
    
    if (!$this->Authorization->can($this)) {
        return $this->redirect('/');
    }

    Unfortunately, it does not work as expected in this lifecycle stage.

     Method `canLogin` for invoking action `login` has not been defined in `App\Policy\UsersControllerPolicy`.
    Authorization\Policy\Exception\MissingMethodException
    
  5. alphp commented on Aug 29, 2026

    @alphp
    Author

    I like the method of using the existing skipAuthorization configuration variable.

    The method name skipAuthorizationActions is good, but it could also be authorizeAction or authorizeController.

    The performCheck method is where you need to add the check to see if the called action skips authorization.

    I used the following code:

            $skipAuthorization = $this->checkAction($action, 'skipAuthorization');
            if ($skipAuthorization) {
                $this->skipAuthorization();
    
                return true;
            }
  6. LordSimal commented on Aug 29, 2026

    @LordSimal
    Member

    I can undestand why you want to introduce this feature, but I also want to explain why the current implementation behaves like it does.

    By default, if you have the authorization plugin active, you HAVE to either do some sort of authorization check OR call $this->Authorization->skipAuthorization() inside the controller action.

    If we wouldn't require this behavior it would be possible for new controller actions to not go through the authorization step and therefore could possibly lead to unwanted behavior. AKA we follow the "least privilege principle" and therefore block every controller action, which does not go through that logic (either directly in the controller action or indirectly via events aka the beforeFilter method/hook)


    Your proposal seems fair to me, as it would introduce a new/easier way of defining controller specific logic to skip authorization for certain actions without having the need to do it globally for the whole app.

    I'd just like to see docs explaining the different ways of skipping authorization as well so users have an easier time knowing when to use which apporach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions