Create an Admin middleware for Laravel with spatie/laravel-permission

Lucas Fiege
Jul 3, 2019 · 7 min read

Note: This tutorial is for Laravel 7 or earlier versions

Image for post
Image for post
Photo by CMDR Shane on Unsplash

Although there are many articles about this topic, I decided to document this in a post for my future self and to share with all of you the approach I usually use to separate an application depending on specific roles.

Middleware provide a convenient mechanism for filtering HTTP requests entering your application. For example, Laravel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.

Additional middleware can be written to perform a variety of tasks besides authentication. A CORS middleware might be responsible for adding the proper headers to all responses leaving your application. A logging middleware might log all incoming requests to your application.

There are several middleware included in the Laravel framework, including middleware for authentication and CSRF protection. All of these middleware are located in the app/Http/Middleware directory.

Creating a custom Admin middleware in Laravel

Spatie/laravel-permission is great package developed by Spatie team that allows you to manage user permissions and roles in a database.

For this example we are going to install the package and create a custom middleware to group our administration routes into a new single route file under the same access control logic for all admin routes.

I will simplify the example in this post to use only two roles: Admin and User (without assigning specific permissions).

First of all, you must fill your .env file with a new database configuration.

This package can be used in Laravel 5.4 or higher. If you are using an older version of Laravel, take a look at the v1 branch of this package.

You can install the package via composer:

composer require spatie/laravel-permission

The service provider will automatically get registered. Or you may manually add the service provider in your config/app.php file:

'providers' => [
// ...
Spatie\Permission\PermissionServiceProvider::class,
];

You can publish the migration with:

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations"

After the migration has been published you can create the role- and permission-tables by running the migrations:

php artisan migrate

Optionally you can publish the config file with:

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config"

add the Spatie\Permission\Traits\HasRoles trait to your User model(s):

use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
// ...
}

Make sure of the Laravel authentication feature is present, if not, you can setup this with the artisan command:

php artisan make:auth

Now add some users to your application, optionally you can use a Seeder to achieve this. For example, create a new RolesAndPermissionsSeeder by running:

php artisan make:seeder RolesAndPermissionsSeeder

Now paste the following code into the new seeder

public function run()
{
// Reset cached roles and permissions
app()['cache']->forget('spatie.permission.cache');


Role::create(['name' => 'user']);
/** @var \App\User $user */
$user = factory(\App\User::class)->create();

$user->assignRole('user');
Role::create(['name' => 'admin']);

/** @var \App\User $user */
$admin = factory(\App\User::class)->create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);

$admin->assignRole('admin');
}

Don’t forget to import the Role class with use Spatie\Permission\Models\Role. Now we have two different users with different roles.

Note: How we have relied on the UserFactory class, the default password for both is password (in Laravel 5.8, for earlier versions the default password is secret)

You can now seed your database with this command

php artisan db:seed --class=RolesAndPermissionsSeeder

We are ready to create a new middleware to separate admin routes of user routes. It should be noted that an administrator can access the routes of a normal user, but not in an inverse way.

To achieve this quickly, I will rely on the use of automated tests using the integrated phpunit on Laravel:

php artisan make:test RolesAccessTest

And add the following tests:

<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;

class RolesAccessTest extends TestCase
{
/** @test */
public function user_must_login_to_access_to_admin_dashboard()
{
$this->get(route('admin.dashboard'))
->assertRedirect('login');
}

/** @test */
public function admin_can_access_to_admin_dashboard()
{
//Having
$adminUser = factory(User::class)->create();

$adminUser->assignRole('admin');

$this->actingAs($adminUser);

//When
$response = $this->get(route('admin.dashboard'));

//Then
$response->assertOk();
}

/** @test */
public function users_cannot_access_to_admin_dashboard()
{
//Having
$user = factory(User::class)->create();

$user->assignRole('user');

$this->actingAs($user);

//When
$response = $this->get(route('admin.dashboard'));

//Then
$response->assertForbidden();
}

/** @test */
public function user_can_access_to_home()
{
//Having
$user = factory(User::class)->create();

$user->assignRole('user');

$this->actingAs($user);

//When
$response = $this->get(route('home'));

//Then
$response->assertOk();
}

/** @test */
public function admin_can_access_to_home()
{
//Having
$adminUser = factory(User::class)->create();

$adminUser->assignRole('admin');

$this->actingAs($adminUser);

//When
$response = $this->get(route('home'));

//Then
$response->assertOk();
}
}

Obviously these assertions could be improved by adding others to check views and / or content that should be shown in those sections, but for the purposes of this post, these tests are sufficient.

If you run this test now, you will get the following errors:

./vendor/bin/phpunit --filter RolesAccessTest
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.
EEE.. 5 / 5 (100%)Time: 219 ms, Memory: 18.00MBThere were 3 errors:1) Tests\Feature\RolesAccessTest::user_must_login_to_access_to_admin_dashboard
InvalidArgumentException: Route [admin.dashboard] not defined.
...ERRORS!
Tests: 5, Assertions: 2, Errors: 3.

Let’s start writing the code so that this test passes:

Create a new temporary route into the routes/web.php file, your file will look like this:

Route::get('/', function () {
return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Route::get('/admin/dashboard', function(){
return 'Wellcome Admin!';
})->name('admin.dashboard');

Now, re-run the test:

./vendor/bin/phpunit --filter RolesAccessTest
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.
F.F.. 5 / 5 (100%)Time: 228 ms, Memory: 18.00MBThere were 2 failures:1) Tests\Feature\RolesAccessTest::user_must_login_to_access_to_admin_dashboard
Response status code [200] is not a redirect status code.
Failed asserting that false is true.
...2) Tests\Feature\RolesAccessTest::users_cannot_access_to_admin_dashboard
Response status code [200] is not a forbidden status code.
Failed asserting that false is true.
...FAILURES!
Tests: 5, Assertions: 5, Failures: 2.

At this point, we have 2 failures only, the admin route is a public route and is not restricted to the Admin role only, let’s fix it in a few steps.

Go to the app\Providers\RouteServiceProvider. In the map method add a new function for map the Admin routes:

public function map()
{
$this->mapApiRoutes();

$this->mapWebRoutes();

$this->mapAdminRoutes();

//
}

and now implement the new mapAdminRoutes method inside the provider:

protected function mapAdminRoutes()
{
Route::middleware('admin')
->namespace($this->namespace)
->group(base_path('routes/admin.php'));
}

Note: optionally, I recommend separating also the namespace of the controllers that will be used by the admin routesand that in addition the users must have the admin role

->namespace($this->namespace . '\\Admin')

Add a new file into routes filder called admin.php and move the route for admins inside of this new file:

<?phpRoute::get('/admin/dashboard', function(){
return 'Welcome Admin!';
})->name('admin.dashboard');

Open the app\Http\Kernel class and find the $routeMiddleware attribute and add two new middleware that belong to spatie/laravel-permission package :

protected $routeMiddleware = [
...
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
];

And in the $middlewareGroups attribute add this new admin middleware group:

protected $middlewareGroups = [
'web' => [
....
],

'admin' => [
'web',
'auth',
'role:admin'
],

'api' => [
...
],
];

This group specifies that the middleware will make use of the web and auth middlewares, and that in addition the users must have the admin role. This middleware group was indicated into mapAdminRoutesmethod into our RouteServiceProvider.

Now you can re-run the tests and check the results

./vendor/bin/phpunit — filter RolesAccessTest
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.
….. 5 / 5 (100%)Time: 215 ms, Memory: 18.00MBOK (5 tests, 6 assertions)

Now we have the new middleware working correctly to restrict access to administration routes only to the corresponding users. You can also perform a manual check with the created users in the Seeder

Image for post
Image for post
Admin user successfully logged in into the admin routes

And if you try to enter in the admin routes with the normal user you will get an Forbidden response:

Image for post
Image for post
Normal user cannot enter to admin routes

With this approach you will get a clear separation for the Admin routes and the normal User routes. We only needed to:

  • Install and configure spatie/laravel-permission
  • Create and assign desired Roles to Users
  • Create a new admin middleware group in the app\Http\Kernel class
  • Create a new mapAdminRoutes into the RouteServiceProvider to map a new routes/admin.php file and assign it to the new admin middleware group

And some advantages of this approach are the separations of concerns for different app components:

  • Separated routes files
  • Separated Controllers with a custom namespace

And also you can use this approach to separate resources files as assets or layouts and view files.

As we have guided our development by the use of automated tests (TDD), we have not needed to manually test with each user of our application during the development, which is also another great advantage.

The Startup

Medium's largest active publication, followed by +755K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store