If you do only have roles for your users – such as admin, contributor and user – and you want to restrict access to routes only, advanced (and excellent) packages may be too heavy for you.
Let me show you how to solve this authorization problem very easily.
Your user model needs an extra field to store the role of every user. Let's add a string field which will hold values such as user
, manager
or admin
. Every user needs a role, so this field is not nullable and has a default of user
. Of course you have more fields in your user model, I'll add an email field to the user for demonstration purposes.
// Create migration
// create_users_table.php
Schema::create('users', function (Blueprint $table) {
...
$table->string('email')->unique();
$table->string('role')->default('user');
...
}
Let's create a test user, using Laravel's tinker app, test it's default role and upgrade it to admin
.
php artisan tinker
// Create test user
$user = App\User::create(['email' => 'test01@test.test']);
$user->role; // 'user'
// Upgrade user to admin
$user->role = 'admin';
$user->save();
Until now, the user role is just a string and does not do anything. Let's change this by generating a custom middleware. Middlewares can be attached to routes and control who has access to those routes.
For example, to restrict the access to /logged-in-users-only
to logged in users, you would create the following function in your routes file:
// routes/web.php
Route::get('logged-in-users-only', function () {
return 'Only logged in users can see this';
})->middleware('auth');
We can restrict routes to user roles with a custom middleware:
php artisan make:middleware UserRoles
This will create a middleware file that we can customize:
// Http/Middleware/UserRoles
public function handle($request, Closure $next, ...$roles)
{
return collect($roles)->contains(auth()->user()->role) ? $next($request) : back();
}
This handle()
method will only allow people access the route if their role is in the list of allowed roles in the protected route.
Here is the whole return
one-liner in natural language:
If the collection of rules contains the logged in user's role, then allow and process the request, otherwise redirect the user back.
-
...$roles
allows to pass multiple roles at once, the resulting$roles
is an array with all parameters - With
collect()
I create a collection out of the array, so I can use collection methods and chain them, such ascontains()
-
contains()
will check whether the role is in the collection -
return CONDITION ? THEN : ELSE
is called a ternary operator and a shorter version ofif (CONDITION) { then } else { }
To make use of this middleware, we need to register it in the kernel:
// Http/Kernel.php
use App\Http\Middleware\UserRoles;
...
protected $routeMiddleware = [
...
'roles' => UserRoles::class,
];
To only allow users with role admin
to a route, you create it like the following:
// routes/web.php
Route::get('types', function () {
return 'Access granted';
})->name('test.roles')->middleware('roles:admin');
And because we allow to pass more than one role (remember the ...$roles
) to the middleware, we can allow several roles to access the same route. Let's also give the manager
access to this route:
// routes/web.php
Route::get('types', function () {
return 'Access granted';
})->name('test.roles')->middleware('roles:admin,manager'); // no space after comma!
One risk with this approach is that you pass roles around as strings. You always need to remember which roles you have and how you named them. A small typo such as adnin
instead of admin
will be hard to spot and renaming a role (e.g. manager
to become editor
) can easily break your system.
In the next iteration of this article, I will introduce Enums
to help reducing this risk, but for now this solution is lean and will solve your basic needs.
P.S. If you have suggestions on refactoring or improving, please leave a comment
Top comments (7)
I guess we can use just
back()
instead ofredirect()->back()
True, thanks for pointing out. I already fixed it!
Hi Martin, great article. I am trying to make this work with Laravel 8. I think there have been changes to the way arrays work. I wish to send a few roles through the route middleware per your example, but only the first is being received. Ie, if i send ->name('test.roles')->middleware('roles:admin'); it works fine, but if I send ->name('test.roles')->middleware('roles:manager,admin'); only manager is received. Any ideas? Thanks in advance. Steve
Thanks martin
Do we need the ->name() or can we leave it out?
You could leave that out, but I advise to always name routes. In your tests, you can write
$this->get(route('name')
and this will still work even if you change the URL from e.g./old
to/new
.Exactly what I need. Thanks! :-)