Repository navigation
Description
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
- API Consistency: Alignment with how
AuthenticationComponent::allowUnauthenticated()works. - Cleaner Controllers: Avoids cluttering public actions with repetitive
$this->Authorization->skipAuthorization()calls. - Better DX: Centralizes access control rules in
beforeFilterright next to authentication rules.
Additional Notes
- Naming Conventions: I am completely open to suggestions regarding the names of the
allowUnauthorized()method and the$unauthorizedActionsproperty 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
dereuromark commentedon Aug 28, 2026
AuthorizationComponent has a skipAuthorization config key and an authorizeAction() handler bound to Controller.startup:
Real gap = no fluent setter mirroring authorizeModel(string ...$actions) maybe. But it is 6 lines so far.
alphp commentedon Aug 29, 2026
Thank you for the quick feedback!
You are completely right about the
skipAuthorizationconfig key. However, the limitation of configuring it inside$this->loadComponent()(usually inAppController::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.
AppControllershouldn't need to know thatUsersControllerhas averifyEmailaction, or thatBillingControllerhas awebhookaction that bypasses authorization.To test the current limitations, I tried dynamically modifying the configuration inside
UsersController::beforeFilter()right before calling manual checks:As expected, this does not work. Because
skipAuthorizationis tightly bound to theController.startupevent handler, modifying the config array at runtime insidebeforeFilteris 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 likeallowUnauthorized()is architecturally the correct place for it.If you agree that adding a fluent setter to dynamically append actions to
skipAuthorizationimproves the DX and architecture, I'd be happy to work on a PR for it!dereuromark commentedon Aug 29, 2026
What do u think about my PR?
alphp commentedon Aug 29, 2026
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:Unfortunately, it does not work as expected in this lifecycle stage.
alphp commentedon Aug 29, 2026
I like the method of using the existing
skipAuthorizationconfiguration variable.The method name
skipAuthorizationActionsis good, but it could also beauthorizeActionorauthorizeController.The
performCheckmethod is where you need to add the check to see if the called action skips authorization.I used the following code:
LordSimal commentedon Aug 29, 2026
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
beforeFiltermethod/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.