Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Authz Laravel Package

casbin/laravel-authz

Casbin Laravel Authz brings Casbin's powerful RBAC/ABAC access control to Laravel. Define policies, protect routes with middleware, integrate gates, cache decisions, and manage rules via Artisan commands—all with familiar Laravel conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, publish, and migrate before writing any policy code:

composer require casbin/laravel-authz
php artisan vendor:publish --provider="Casbin\Laravel\CasbinServiceProvider"
php artisan migrate          # creates the casbin_rule table
php artisan casbin           # generates a model .conf (ACL / RBAC / RBAC-with-domains / ABAC)

Read these five things, in this order, before building anything:

  1. The README — install + middleware + facade usage.
  2. config/casbin.php — model path, adapter, cache, log. Almost every "why isn't this working" answer lives here.
  3. The generated config/casbin-*.conf model file — this is the actual authorization logic (request/policy/role definitions, matcher, effect). The PHP code is just a driver.
  4. vendor/casbin/laravel-authz/src/Middlewares/CasbinMiddleware.php — to see exactly what it passes as subject, object, and action. Don't assume.
  5. The php-casbin docs at casbin.org — the Enforcer facade is a thin wrapper; the API is 1:1.

First use case: gate an RBAC route group on path + HTTP method.

// routes/web.php
Route::middleware(['auth', 'casbin'])->group(function () {
    Route::get('/articles', [ArticleController::class, 'index']);
    Route::post('/articles', [ArticleController::class, 'store']);
});

// seeder or tinker
use Casbin\Laravel\Facades\Enforcer;

Enforcer::addRoleForUser('1', 'writer');        // g, 1, writer
Enforcer::addPolicy('writer', '/articles', 'GET');   // p, writer, /articles, GET
Enforcer::addPolicy('writer', '/articles', 'POST');

Enforcer::enforce('1', '/articles', 'GET');     // true

php artisan tinker + Enforcer::getPolicy() is the fastest sanity check you have — use it constantly.

Implementation Patterns

Wrap the facade in one service, never call it from controllers. You want a single write path so you can flush the policy cache in one place, and a single place to normalize the subject to a string.

final class AuthorizationService
{
    public function subject(User $user): string
    {
        return (string) $user->getKey();     // Casbin compares strings
    }

    public function assignRole(User $user, string $role): void
    {
        Enforcer::addRoleForUser($this->subject($user), $role);
        $this->flushCache();
    }

    public function can(User $user, string $object, string $action): bool
    {
        return Enforcer::enforce($this->subject($user), $object, $action);
    }
}

Use the route middleware for coarse checks, Gates for object-level checks. The middleware is endpoint-shaped (is this user allowed to hit this path with this verb?). Anything that depends on a specific record — "can this user edit this post?" — belongs in a Gate or policy that calls enforce() with the model instance (ABAC):

// AppServiceProvider::boot()
Gate::define('update-post', fn (User $user, Post $post) =>
    Enforcer::enforce((string) $user->getKey(), $post, 'update')
);

// model conf matcher for ABAC:
// m = r.sub == r.obj.Owner || g(r.sub, p.sub) && r.act == p.act

Match routes with keyMatch2, not ==. A policy written as /articles will never match /articles/42. In the model:

[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act

Multi-tenancy via the domains model. Use r = sub, dom, obj, act and pass the tenant explicitly; the stock middleware has no concept of a domain, so write your own middleware or resolve the tenant inside your service:

Enforcer::addRoleForUserInDomain($userId, 'admin', $tenantId);
Enforcer::enforce([$userId, $tenantId, '/articles', 'GET']);

Seed policies as data, not code. Casbin returns false from addPolicy when the rule already exists, so seeders are naturally idempotent. Use addPolicies() for bulk grants instead of N queries, and keep policy changes in migrations/seeders so every environment gets the same rules on deploy.

Enable caching in production. With cache.enabled => true the whole policy set is loaded once per TTL instead of a query per request — required once you have more than a few hundred rules. Use a shared store (Redis) across nodes, and flush after every write.

Octane: the enforcer is a container singleton; under Octane it survives between requests and serves stale policies. Forget the instance per request via a listener, or accept cache TTLs as your staleness budget.

Testing: set config(['casbin.cache.enabled' => false]) before the enforcer is first resolved (the singleton freezes config), use RefreshDatabase, and seed the handful of rules each test needs. For pure unit tests, build a Casbin\Enforcer against a file/array adapter instead of going through the facade.

Gotchas and Tips

  • Verify the middleware contract before writing a single policy. It derives the subject from the request user, the object from the request path, and the action from the HTTP method. If you need route names, a tenant, or a different verb mapping, subclass it and register your own alias — fighting the default mapping produces policies that look right and never match.
  • Guests are denied. No authenticated user means no subject. Always stack auth before casbin; if you need anonymous-but-authorized access, use an explicit guest subject.
  • Type mismatches silently fail. Auth::id() is an int, policies are strings. Cast the subject everywhere — this is the #1 cause of "it works in tinker but not in the app."
  • The default config uses a Closure for the model path, which makes php artisan config:cache fail ("Your configuration files are not serializable"). Replace it with a plain string path (or bind it in a service provider) and run config:cache in CI so you find out before production does. The .conf file itself is not in the config cache — it must ship with the code on every node.
  • Stale cache after out-of-band writes. Editing casbin_rule by hand, or from another service, leaves the cached blob intact. Funnel writes through your service and flush; there's an artisan command for clearing the policy cache (check php artisan list | grep casbin for the exact name in your version) and you should call it from your deploy/policy-sync step.
  • casbin_rule.v0..v5 are varchar(255). Long ABAC strings or JSON blobs won't fit. Widen the columns or normalize before you need them.
  • Deleting a user leaves orphan policies. With auto-increment IDs, a recycled ID inherits the old user's roles — a real privilege-escalation path. Hook user deletion (model observer) and remove g/p rows.
  • Deny-by-default. No matching policy is a false, not an exception. Need a root fallback? Add r.sub == 'root' to the matcher or a Gate::before, don't special-case it in every controller.
  • Deny-override is a model change, not a code change. some(where (p.eft == allow)) vs !some(where (p.eft == deny)) — understand the [policy_effect] block before promising anyone "explicit deny wins."
  • Debugging order: Enforcer::getPolicy() and getRolesForUser($id) → check the matcher and [role_definition] field count → check the cache is off/cleared → then suspect your code. Most failures are model-conf typos.
  • 403 shape for APIs. The middleware aborts; if you need JSON, override the response in your exception handler (Laravel 11+: bootstrap/app.php) rather than duplicating authorization in controllers.
  • Laravel 11/12 skeletons: if casbin doesn't resolve as a route middleware alias, register it manually in bootstrap/app.php — the provider's registration is the only thing standing between you and a very confusing InvalidArgumentException.
  • Pin and check the underlying Casbin version (composer show casbin/casbin). The wrapper tracks Casbin v4; casbin.org docs cover v3 too, and methods like enforceEx/addPoliciesEx differ between them.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource