How to Implement Role-Based Authentication in Laravel with Sanctum: A Step-by-Step Guide
Tags: api, laravel, webdev, backend
Published: 2023-04-13 06:35:15

Role-based authentication is a crucial aspect of web application development, especially when it comes to managing user access to different parts of the system. In Laravel, Sanctum is a popular package for implementing API authentication, including role-based access control. This guide will provide a step-by-step approach to implement role-based authentication in Laravel using Sanctum. 

Our API will have users with two types of roles: `admin` and `moderator`. Admin will create moderators and Moderators will basically perform all other operations that may be needed.

**Step 1: Setting up our project.**

- Create a laravel project by running the command `composer create-project laravel/laravel myProjectName` anywhere you want to create your project.

- After that create your database in any type of database management system of your choice then head over to `.env` file of your project and configure the following 
:

```
DB_CONNECTION=pgsql //fill according to your database
DB_HOST=127.0.0.1
DB_PORT=5432 //fill according to your database
DB_DATABASE=databaseName
DB_USERNAME=yourUsername
DB_PASSWORD=yourPassword
```

- Now, install sanctum to your project by running the command `composer require laravel/sanctum` on  your project's root directory.
- Next, you should publish the Sanctum configuration and migration files using the command `php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"`

- After that, you should replace Sanctum's middleware of your api middleware group within your application's `app/Http/Kernel.php` file with:
 

```
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
```
Finally, run `php artisan migrate`

**Step 2: Create users table and User Model**

In your migrations folder open the migration file `create_users_table` and on the up function replace the existing code with: 

 

```
Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('username')->unique();
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->enum('role', [ 'admin', 'moderator'])->default('admin');
            $table->timestamps();
        });
```

- What this does is it creates a users table with the columns username, email, password, role and others as described in the function.

- `$table->enum('role', [ 'admin', 'moderator'])->default('admin')`; - This adds a "role" column to the "users" table with an enumerated data type and a default value of "admin". The "role" column can only contain values of "admin" or "moderator". Now, what we are doing here is that if the the registration request won't have a role in it then by default that user will have a role of admin.

Run `php artisan migrate` to create the users table.

Then,create a User model with fillable fields: In Laravel, you can create a User model by running the `php artisan make:model User` command in the terminal. Once you've done that, you can define the model's fillable fields by adding the following code to the model file:

```
protected $fillable = [
    'username',
    'email',
    'password',
    'role'
];
```

**Step 3: Create a middleware to check roles of users.**

A middleware simply sits between the server and your request, it checks to see if you're allowed to access that information or perform that action.

We will create a `checkRole` middleware by running the command `php artisan make:middleware checkRole`.
Once that is done open the middleware and paste the following code : 

  

```
public function handle(Request $request, Closure $next, ...$roles)
    {
   
        $user = $request->user();

        if (! $user || ! in_array($user->role, $roles)) {
            abort(403, 'Unauthorized');
        }

        return $next($request);
 
    }
```

- What this does is it checks if the authenticated user has a role that is allowed to access a certain route. If the user does not have the required role, it aborts the request and returns a 403 (Forbidden) error message. Otherwise, it passes the request to the next middleware in the chain.

- Now register the middleware you just created by adding it in the array of `routeMiddleware` in your `kernel.php` as `'restrictRole' => 
\App\Http\Middleware\CheckRole::class,`

**Step 4: Create Authentication Controller**

This controller will handle our registration and login logic.

- But remember, since only admin can create moderators, Then the registration endpoint must be protected because we don't want other users to assign roles to themselves. Therefore we will create an initial default admin who can then create other admins and moderators.

- To do this we need to create a seeder called defaultAdmin  by running the command `php artisan make:seeder defaultAdmin`. Once that is done open the file and paste the following code to create a default initial user:
 

 

```
public function run()
    {
        User::create(
            [
            'username' => 'admin',
            'email' => 'admin@jf.com',
            'password' => bcrypt('password'),
            'role' => 'admin',
            ]
            );
    }

```

- Now you can proceed creating the Authentication controller by running `php artisan make:controller AuthController
`

- Open the file and paste the following code to handle registration and login
: 

 

```
public function register( Request $request){
        $data = $request->validate([
            'username' => 'required|unique:users',
            'email' => 'required|unique:users',
            'password' => 'required|confirmed',
            'role' => 'in:admin,moderator',
        ]);
        
        // Set default role to 'admin' if not provided in the request
        $userData = array_merge($data, ['role' => $data['role'] ?? 'admin']);
 
        
        // Mass assign the validated request data to a new instance of the User model
        $user = User::create($userData);
        $token = $user->createToken('my-token')->plainTextToken;

        return response()->json([
            'token' =>$token,
            'Type' => 'Bearer'
        ]);
    }

    public function login(Request $request)
    {
        $fields = $request->validate([
            'username' => 'required',
            'password' => 'required',
        ]);
    
        $user = User::where('username', $fields['username'])->first();
    
        if (!$user || !Hash::check($fields['password'], $user->password)) {
            return response([
                'message' => 'Wrong credentials'
            ]);
        }
    
        $token = $user->createToken('my-token')->plainTextToken;
    
        return response()->json([
            'token' => $token,
            'Type' => 'Bearer',
            'role' => $user->role // include user role in response
        ]);
    }
```
Code Explanation

- The register function first uses the validate method of the request object to validate the input data against some rules defined in an array.

- If the validation passes, the function continues by merging the validated data with a default value for the role field, if not provided in the request.


- The function then creates a new instance of the User model with the validated and merged data, using the create method, which performs a mass assignment of the input data to the new user object.


- After creating the user, the function generates an access token for the user using the `createToken` method of the user object, and returns a JSON response containing the token and its type. The token can be used by the client to authenticate subsequent requests to protected routes on the server
.

- The login function first validates the input data (the username and password fields) using the validate method of the request object. The function then retrieves a user instance from the database based on the provided username. If the user doesn't exist or the password is incorrect, it returns a response with the message "Wrong credentials".

 

- If the user exists and the password is correct, the function generates an access token for the user using the` createToken` method of the user object and returns a JSON response containing the token, its type, and the user's role. The token can be used by the client to authenticate subsequent requests to protected routes on the server.

**Step 5: Create routes**

Open `api/routes.api` file and paste the following code 

```
//public route
Route::post('/auth/login', [AuthController::class, 'login']);
//protected route
Route::group(['middleware' => ['auth:sanctum']], function(){
 Route::post('/auth/register', [AuthController::class, 'register'])->middleware('restrictRole:admin');
Route::get('/users', [PostController::class, 'show'])->middleware('restrictRole:admin');
Route::put('/users/{id}', [PostController::class, 'update'])->middleware('restrictRole:moderator');
});
```

- You may notice we have added two more routes, a `GET `request to users and a `PUT` request to users. We want only admin to have access to the endpoint that gets users and only moderators to have access to the endpoint that updates users.

- Now, as you have seen in the above code all these routes have been placed within a single function and under one group. A route group allows you to group several routes together and apply the same middleware, namespace, prefix, or other common attributes to them.

- In the provided code, `Route::group` is a function that creates a new route group with middleware. The middleware in this case is `auth:sanctum`, which indicates that any route within this group requires authentication using the Sanctum middleware.

- In the protected routes we have used the `->middleware()` method which assigns middleware to a specific route in Laravel. In the provided code,we have assigned to the route our custom  `restrictRole` middleware which we created and registered to the `routeMiddleware` array of the `kernel.php` file.

- It checks if the authenticated user has any of the specified roles. If the user has one of the specified roles, the request continues to the route's controller or closure. If the user does not have any of the specified roles, the middleware will return a response indicating that the user is not authorized to access the route.

- Since all this is clear now lets head over to our `AuthController` and add functions `show` and `update` which have been used by the last two routes.
In your `AuthController` add this code:

```
// You could basically write some functional code here to 
// perform these operations
public function show(){
return " All users are shown here";
}

public function update(Request $request , $id){
return " All updates on users are done here";
}
```

- From here an admin could see all users and moderators could update them by their id.
Since we already have one default admin, you can now login and register other users(admin/moderators) and try to access the restricted endpoints.

Here is a sample registration and login requests.

```
{
"username":"James",
"email": "jamie@gmail.com",
"password": "123456",
"password_confirmation": "123456",
"role" : "moderator",
}
```

Login request

```
{
"username": "James",
"password": "123456"
}
```

Here you will create a moderator and after login the user can access the routes which he has authority to.

That's all for today. Hope you got something from this one . Enjoy !!