cakephp/authorization
CakePHP Authorization is a policy-based authorization plugin for CakePHP, offering middleware integration, request-level permission checks, and reusable policy classes. It helps centralize access rules and keep controllers clean, making role- and resource-based authorization easier to implement cons...
composer require cakephp/authorization
Register the middleware in src/Application.php, after AuthenticationMiddleware (the package reads the identity it attaches):
use Cake\Authorization\Middleware\AuthorizationMiddleware;
use Cake\Authorization\Middleware\RequestAuthorizationMiddleware;
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
->add(new ErrorHandlerMiddleware())
->add(new AuthenticationMiddleware($this))
->add(new AuthorizationMiddleware()) // service + policy resolver
->add(new RequestAuthorizationMiddleware()); // request-level policy checks
return $middlewareQueue;
}
Look here first:
Cake\Authorization\AuthorizationService — the can() / authorize() / applyScope() API you'll call from controllers.Cake\Authorization\PolicyResolver — how entity/table/request classes map to Policy classes (App\Policy\{EntityClass}Policy).Cake\Authorization\Policy\BeforePolicyInterface — the escape hatch for "superuser bypasses everything".First use case — gate a controller action on an entity:
// src/Policy/ArticlePolicy.php
class ArticlePolicy
{
public function canEdit(IdentityInterface $user, Article $article): bool
{
return $user->getIdentifier() === $article->user_id;
}
}
// Controller
public function edit($id)
{
$article = $this->Articles->get($id);
$this->Authorization->authorize($article, 'edit'); // throws ForbiddenException
// ...
}
Policy method naming convention. can{Action} for object-level checks. The action name is case-insensitive (edit → canEdit). Return bool, AuthorizationResult, or null (undecided). Use AuthorizationResult::allowIf() when checks are expensive or composed.
Table policies + scopes for indexes. The dominant pattern is scopeIndex on the table policy, applied via the service rather than a can* method:
// src/Policy/ArticlesTablePolicy.php
public function scopeIndex(IdentityInterface $user, SelectQuery $query): SelectQuery
{
return $user->get('is_admin')
? $query
: $query->where(['Articles.user_id' => $user->getIdentifier()]);
}
// Controller index()
$query = $this->Authorization->applyScope($this->Articles->find());
applyScope() requires the scope method to exist on the resolved policy, so put scopeIndex on ArticlesTablePolicy and canEdit/canDelete on ArticlePolicy (entity).
Request policies for route access. RequestPolicy short-circuits before your controller runs:
class RequestPolicy
{
public function canAccess(IdentityInterface $user, ServerRequest $request): bool
{
return $user->get('is_active');
}
public function canBypass(IdentityInterface $user, ServerRequest $request): bool
{
return $user->get('is_admin');
}
}
canBypass is checked first and, if true, skips all downstream authorization in that request — great for admin back offices, dangerous if broad.
Non-throwing checks for UI. Use the service directly so views can hide buttons without exceptions:
$result = $this->Authorization->can($article, 'edit');
if ($result->getStatus()) { /* render edit link */ }
Identity decorators / custom service. Bind AuthorizationServiceInterface and PolicyResolverInterface in the container to swap in multi-tenant policy lookup or an identity that carries tenant scope:
// src/Application.php
$container->add(AuthorizationServiceInterface::class, TenantAuthorizationService::class);
Testing. PolicyTestTrait lets you assert policy methods directly without spinning up a request:
use Cake\Authorization\TestSuite\PolicyTestTrait;
$result = $this->assertPolicyAllows('edit', $article, $editor);
Cake\Http\ServerRequest, IdentityInterface, and Cake conventions. Do not expect it to drop into Slim/Laminas; for those use mezzio-authorization or psr7-session middleware.AuthorizationMiddleware before AuthenticationMiddleware → MissingIdentityException on every request. RequestAuthorizationMiddleware must run at routing scope — put it in Application::routes() via $routes->registerMiddleware(), not only in middleware(), or it never fires for route-level checks.scopeIndex doesn't fail closed. applyScope() throws if the method is absent, but a policy that returns the unfiltered query leaks rows — audit scopes like you'd audit SQL.can* returning null/no method = denied. There is no silent allow. Verify your method name casing matches the action, and remember canIndex on entity policy is not used for controller indexes—use scopeIndex on the table policy.skipAuthorization() is a footgun. It disables both middleware and controller checks for the rest of the request. Prefer canBypass in a RequestPolicy so the exception is centralized and auditable.MissingIdentityException means the identity middleware didn't run or the user isn't logged in; ForbiddenException means a policy denied. Map them to 401 vs 403 in your error handler — they are not interchangeable.try/catch around authorize() in a base controller, or decorate PolicyResolverInterface to log (identity id, policy class, method, result) — invaluable when a policy appears both to run and to be ignored.PolicyResolverInterface (custom class-to-policy mapping), BeforePolicyInterface (root bypass), AuthorizationResult/ResultInterface (composable decisions), and the AuthorizationMiddleware identityAttribute option if your identity lives under a non-default request attribute.can() call or checks will use the old object.How can I help you explore Laravel packages today?