aurorawebsoftware/aauth
AAuth is a Laravel package that adds a configurable authentication and authorization layer to your application, making it easier to manage guards, access control, and user permissions with minimal setup.
Install and publish
composer require aurorawebsoftware/aauth
php artisan vendor:publish --provider="AuroraWebSoftware\AAuth\AAuthServiceProvider"
php artisan migrate
The publish step is the important one — it drops config and migration files into your app. Read the config file first; it usually contains the table names, guard names, and any "super admin" bypass flag you'll need to align with your existing users table.
Where to look first
config/aauth.php (or similarly named) — table names, model bindings, cache settings.users and whether they assume a specific primary key type (bigIncrements vs uuid).boot() — this is where Blade directives, middleware aliases, and Gate integration are typically registered.First use case: gate one route
Add the package trait to your User model:
use AuroraWebSoftware\AAuth\Traits\HasAAuth;
class User extends Authenticatable
{
use HasAAuth;
}
Seed a role and a permission, attach them, then protect a route:
Route::get('/reports', fn () => view('reports'))
->middleware('aauth.permission:reports.view');
If that works end-to-end, you've validated the migration, the trait, the guard wiring, and the middleware alias in one pass. Do this before writing any business logic — auth packages fail loudly at this boundary and it's cheap to fix now.
Seeder-driven permission catalogue
The most maintainable pattern with any RBAC package is to treat permissions as data you define in code and roles as data you edit in the admin UI. Keep a canonical list:
// database/seeders/PermissionSeeder.php
$permissions = [
'reports.view', 'reports.export',
'users.invite', 'users.deactivate',
'billing.view', 'billing.refund',
];
foreach ($permissions as $name) {
AAuth::permission()->firstOrCreate(['name' => $name]);
}
Run this in every environment including production (--class=PermissionSeeder). It's idempotent, so it's safe on deploy, and it means "what permissions exist" is always answerable from git.
Controller checks vs. policies
Use the package's facade for coarse checks at the top of an action, and Laravel policies for per-record authorization:
public function refund(Invoice $invoice)
{
abort_unless(AAuth::hasPermission('billing.refund'), 403);
$this->authorize('refund', $invoice); // per-record logic in your Policy
}
This split keeps the package out of your domain rules. Model-specific authorization stays in Laravel's native policy system, which is easier to test and doesn't tie your business logic to a third-party API.
Blade conditional rendering
Most packages register a directive. Use it, but don't rely on it for security — it's a UI convenience only:
@aauth('billing.refund')
<button wire:click="refund">Refund</button>
@endaauth
The same check must exist server-side in the Livewire component or controller. A hidden button is not authorization.
Route groups by permission
For admin areas, group routes so you don't repeat middleware:
Route::middleware(['auth', 'aauth.permission:admin.access'])
->prefix('admin')
->group(base_path('routes/admin.php'));
Keep permission names namespaced (admin.users.edit) so a route group maps to a permission prefix and you can audit coverage by grepping the routes file.
Entity/tenant scoping
If the package supports scoping permissions to a model instance (many Laravel RBAC packages do, via a polymorphic pivot), use it for multi-tenant apps rather than creating duplicate global permissions per tenant. Pattern:
AAuth::for($organization)->hasPermission('billing.refund')
Verify the exact API against the README — this is the single most version-sensitive part of these packages, and getting it wrong means permissions silently resolve against the wrong scope.
Testing
Write a small helper so tests stay readable:
protected function actingAsWithPermissions(array $perms, ?User $user = null): User
{
$user = $user ?? User::factory()->create();
$role = AAuth::role()->create(['name' => 'test-'.uniqid()]);
$role->syncPermissions($perms);
$user->assignRole($role);
return $this->actingAs($user);
}
Because the package caches permission lookups, flush the cache in setUp() or your tests will leak state across cases.
Cache invalidation is the #1 source of "it works locally" bugs. Authorization packages cache the permission graph per user because it's expensive to query on every request. If you assign a role in an admin panel and the change doesn't take effect until the user re-logs in, it's the cache. Look for a flush/forget method on the facade and call it from your role-assignment actions, or disable the cache in local/test environments via config.
Watch for N+1 on permission checks in loops. AAuth::hasPermission() inside a @foreach over 200 rows may re-query per iteration depending on whether the eager-load is warm. Eager-load the roles/permissions relationship (the trait usually exposes something like ->with('roles.permissions')) or check the permission once before the loop and pass a boolean in.
Guard mismatch. If your app has multiple guards (web, admin, api), the package's middleware may default to one. A user authenticated on admin but checked against web will silently fail hasPermission() and return false rather than throwing — which reads as "permission missing" when the real problem is the guard. Confirm which guard the config points at before debugging your pivot tables.
Super-admin bypass is usually config-driven, not a permission. Many packages short-circuit hasPermission() to true for a flagged user or role. That means hasPermission() can return true for a permission that doesn't exist in the DB. Don't use hasPermission() as an existence check for "does this permission exist" — that's what the permission repository/query is for.
Migration ordering and key types. Published migrations often assume they run after users. If you have a custom users table with a uuid primary key, the package's user_id foreign key column will be the wrong type. Open the migration before running php artisan migrate and adjust — re-running migrations after the fact means dropping and recreating the pivot tables.
Middleware aliases must be registered. Some versions of these packages ship the middleware but expect you to register the alias in bootstrap/app.php (Laravel 11+) or Kernel.php. If you get "Target class [aauth.permission] does not exist", that's the missing registration, not a missing package install.
Prefer permission checks over role checks in application code. Checking hasRole('manager') scatters org-chart assumptions across controllers; checking hasPermission('reports.export') means you can reorganize roles in the admin UI without code changes. Reserve role checks for things that are genuinely about role identity (e.g., "only the owner role can transfer ownership").
Debugging checklist when a check unexpectedly fails:
hasPermission may be an undefined-method error or, worse, resolve to a false stub.)$user->roles()->with('permissions')->get().AAuth::hasPermission() vs Auth::guard('admin')->user()->hasPermission().dot.notation separator?Extension points worth knowing before you need them. Look for an interface or binding for the permission-checker and for the role/permission models in config. If your app needs a custom role model (extra columns, soft deletes, team scoping), the config binding is usually the intended path — but binding it after you have production data means a risky migration, so decide early.
Low adoption signal. With effectively no dependents or community footprint, expect to read the source rather than find answers. Budget an hour to skim src/ before committing: specifically the service provider, the trait, and the middleware. That hour tells you more than the README, and it tells you whether the package's guard/cache assumptions match your app.
Have an exit ramp. Because authorization touches every controller, wrap the package behind a thin app-level service (App\Support\Authorization) that exposes can(User $u, string $permission): bool. Call that everywhere instead of the facade. If you ever need to swap to spatie/laravel-permission or native Gates, you change one file instead of two hundred.
How can I help you explore Laravel packages today?