Laravel Passport Tutorial

When building a web application, it’s often beneficial to divide it into distinct tiers. Typically, there’s a middle-tier API responsible for database interactions, while the web tier comprises a front-end SPA or MPA. This architectural approach fosters loose coupling within the web application, facilitating easier management and debugging over time.

Once the API is established, configuring authentication and state management within a stateless API context may pose certain challenges.

This article explores the implementation of comprehensive user authentication and basic access control in an API using Laravel and Passport. It assumes a certain level of familiarity with Laravel, as it is not intended as an introductory tutorial.

Step 1: Install Laravel / Passport

Here’s the link for the installation of the Laravel passport.

Step 2: Adding Table and Model

Before creating the model and controller, we need to create a migration for the table user and role.

php artisan make:migration create_users_table --create=users
php artisan make:migration create_roles_table --create=roles

User Table Schema:

Schema::create('users', function (Blueprint $table) {
  $table->bigincrements('user_id');
  $table->string('first_name');
  $table->string('last_name');
  $table->string('email')->unique();
  $table->timestamp('email_verified_at')->nullable();
  $table->string('password');
  $table->unsignedBigInteger('role_id')->nullable();
  $table->string('role_name')->nullable();
  $table->rememberToken();
  $table->timestamps();
});    

Role Table Schema (Optional):

Schema::create('roles', function (Blueprint $table) {
  $table->bigincrements('role_id');
  $table->string('role_name')->unique();
  $table->string('role_desc')->nullable();
  $table->timestamps();
});

Create a model for User and Role

php artisan make:model User 
php artisan make:model Role

User Model (App/Models/User)

    /**
     * The attributes that are mass assignable.
     * @var array<int, string>
     */
    protected $fillable = [
        'first_name',
        'last_name',
        'email',
        'password',
        'role_id',
        'role'
    ];
  • Use the following libraries in User model
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
  • Utilizes several traits: ‘HasApiTokens', ‘HasFactory' and ‘Notifiable‘
use HasApiTokens, HasFactory, Notifiable;
  1. HasApiTokens: This trait is used to enable the user model to issue API tokens for authentication purposes. It provides methods for creating, managing, and revoking API tokens associated with a user. API tokens are commonly used for authenticating API requests made by clients to your application.
  2. HasFactory: The HasFactory trait is used to define model factories for generating dummy data during testing or seeding the database. It provides a convenient way to define the blueprint for creating model instances with predefined attributes.
  3. Notifiable: This trait allows the user model to send notifications via various notification channels supported by Laravel, such as email, SMS, and Slack. By including the Notifiable trait, the User model gains methods for sending notifications and managing notification preferences for each user.

Import the Schema class in App Service Provider (app/Providers/AppServiceProvider.php)

use Illuminate\Support\Facades\Schema

Insert the line at the boot function (public function boot(): void{…}) of the same file.

Schema::defaultStringLength(191);

Apply the migration using this command.

php artisan migrate

Step 3: Create the Necessary Pieces of Middleware

Create JSON Reponse middleware. It will convert all the responses to JSON automatically.

php artisan make:middleware ForceJsonResponse

Modify the handle function of the ForceJsonResponse (App/Http/Middleware/ForceJsonReponse.php)

public function handle($request, Closure $next)
{
    $request->headers->set('Accept', 'application/json');
    return $next($request);
}

Find the $routeMiddleware array in Kernel File (app/Http/Kernel.php) and add the middleware of ForceJsonResponse.

'json.response' => \App\Http\Middleware\ForceJsonResponse::class,

Also add the same file in the $middleware array.

\App\Http\Middleware\ForceJsonResponse::class,

Creating CORS (Cross-origin Resource Sharing)

Create Cors Middleware using the command.

php artisan make:middleware Cors

Add the following codes in Cors middleware (app/Http/Middleware/Cors.php).

public function handle($request, Closure $next)
{
    return $next($request)
        ->header('Access-Control-Allow-Origin', '*')
        ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        ->header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, X-Token-Auth, Authorization');
}

Load the middleware by importing the file in the Kernel (app/Http/Kernel.php) $routeMiddleware array.

'cors' => \App\Http\Middleware\Cors::class,

Also, add this file to $middleware array.

\App\Http\Middleware\Cors::class,

Append the Cors middleware in the route API (routes/api.php).

Route::group(['middleware' => ['cors', 'json.response']], function () {
    // ...
});

All API route will include to this Route Group function.

Step 4: Create User Authentication Controllers for the API

Create ApiAuthController with login, register and logout functions.

php artisan make:controller Auth/ApiAuthController

Import some class to the file of APIAuthController (app/Http/Controllers/Auth/ApiAuthController.php)

use App\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

Add the code for the Register, Login and Logout Function.

public function register (Request $request) {
    $validator = Validator::make($request->all(), [
        'name' => 'required|string|max:255',
        'email' => 'required|string|email|max:255|unique:users',
        'password' => 'required|string|min:6|confirmed',
    ]);
    if ($validator->fails())
    {
        return response(['errors'=>$validator->errors()->all()], 422);
    }
    $request['password']=Hash::make($request['password']);
    $request['remember_token'] = Str::random(10);
    $user = User::create($request->toArray());
    $token = $user->createToken('Laravel Password Grant Client')->accessToken;
    $response = ['token' => $token];
    return response($response, 200);
}
public function login (Request $request) {
    $validator = Validator::make($request->all(), [
        'email' => 'required|string|email|max:255',
        'password' => 'required|string|min:6|confirmed',
    ]);
    if ($validator->fails())
    {
        return response(['errors'=>$validator->errors()->all()], 422);
    }
    $user = User::where('email', $request->email)->first();
    if ($user) {
        if (Hash::check($request->password, $user->password)) {
            $token = $user->createToken('Laravel Password Grant Client')->accessToken;
            $response = ['token' => $token];
            return response($response, 200);
        } else {
            $response = ["message" => "Password mismatch"];
            return response($response, 422);
        }
    } else {
        $response = ["message" =>'User does not exist'];
        return response($response, 422);
    }
}
public function logout (Request $request) {
    $token = $request->user()->token();
    $token->revoke();
    $response = ['message' => 'You have been successfully logged out!'];
    return response($response, 200);
}

Add the functions to the API route file.

Route::group(['middleware' => ['cors', 'json.response']], function () {
  Route::post('/register', [ApiAuthController::class, 'register'])->name('register.api');
  Route::post('/login', [ApiAuthController::class, 'login'])->name('login.api');

  Route::middleware('auth:api')->group(function () {
     Route::post('/logout', [ApiAuthController::class, 'logout'])->name('logout.api');
  });
});

Authentication Test: Creating a User

Required variables for the Post Register end point. (api/register)

{
    "first_name": "First",
    "last_name": "Last",
    "email" : "test@email.com",
    "password" : "password",
    "password_confirmation" : "password"
}

Required variables for Post Login end point. (api/login)

{
    "email" : "1a@gmail.com",
    "password" : "123123123"
}


The success [200] response of the API will display like this below.

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGci...0pZQmPw1WBgzX2N_RNDHlahkkv6xVIwgLE"
}

Accessing Auth Route using token.
 By passing the parameter -H "Authorization: Bearer <token>", where <token> is the authorization token given from the login or register response.
In Postman: where {{token}} is the variable of auth token.

[{
"key":"Authorization",
"value":"Bearer {{token}}",
"description":null,
"type":"text",
"enabled":true
}]

Step 5: Create Password Reset Functionality

Create Forgot Password Controller

php artisan make:controller ForgotPasswordController --resource

Add the sendResetLinkResponse and sendResetLinkFailedResponse function in Forgot Password Controller (App/Http/Controllers/ForgotPasswordController.php).

protected function sendResetLinkResponse(Request $request, $response)
{
    $response = ['message' => "Password reset email sent"];
    return response($response, 200);
}
protected function sendResetLinkFailedResponse(Request $request, $response)
{
    $response = "Email could not be sent to this email address";
    return response($response, 500);
}

Create Reset Password Controller

php artisan make:controller ResetPasswordController --resource

Add resetPassword, sendResetResponse and sendResetFailedResponse functions in the Reset Password Controller((App/Http/Controllers/ResetPasswordController .php))

protected function resetPassword($user, $password)
{
    $user->password = Hash::make($password);
    $user->save();
    event(new PasswordReset($user));
}
protected function sendResetResponse(Request $request, $response)
{
    $response = ['message' => "Password reset successful"];
    return response($response, 200);
}
protected function sendResetFailedResponse(Request $request, $response)
{
    $response = "Token Invalid";
    return response($response, 401);
}

Import these classes into the controllers that you created.

use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

Next, create the Mail Reset Password Notification class by using the command below.

php artisan make:notification MailResetPasswordNotification 

Modify the Mail Reset Password Notification file (app/Notifications/MailResetPasswordNotification.php) using this code.

<?php

namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Lang;
class MailResetPasswordNotification extends ResetPassword
{
    use Queueable;
    protected $pageUrl;
    public $token;
    /**
    * Create a new notification instance.
    *
    * @param $token
    */
    public function __construct($token)
    {
        parent::__construct($token);
        $this->pageUrl = 'localhost:8080';
            // we can set whatever we want here, or use .env to set environmental variables
        }
    /**
    * Get the notification's delivery channels.
    *
    * @param  mixed  $notifiable
    * @return array
    */
    public function via($notifiable)
    {
        return ['mail'];
    }
    /**
    * Get the mail representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return \Illuminate\Notifications\Messages\MailMessage
    */
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $this->token);
        }
        return (new MailMessage)
            ->subject(Lang::getFromJson('Reset application Password'))
            ->line(Lang::getFromJson('You are receiving this email because we received a password reset request for your account.'))
            ->action(Lang::getFromJson('Reset Password'), $this->pageUrl."?token=".$this->token)
            ->line(Lang::getFromJson('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.users.expire')]))
            ->line(Lang::getFromJson('If you did not request a password reset, no further action is required.'));
    }
    /**
    * Get the array representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return array
    */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

Override the sendPasswordResetNotification method that User inherits from the Authenticatable class. (app/Models/User.php)

public function sendPasswordResetNotification($token)
{
    $this->notify(new \App\Notifications\MailResetPasswordNotification($token));
}

Step 6: Create Access Control Middleware

Update the user table by creating update_user_table using the command.

php artisan make:migration update_users_table_to_include_type --table=users

Update the up and down functions add and remove the type column respectively.

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->integer('type');
    });
}
/**
 * Reverse the migrations.
 *
 * @return void
 */
public function down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropIfExists('type');
    });
}

And migrate it to update the table.

php artisan migrate

Add type validation to the $validation array:

'type' => 'integer',

All registered users are “normal users” by default [0], i.e., if no user type is entered.

The Access Control Middleware Itself

Create two pieces of middleware to use for access control: one for admins and one for super-admins.

php artisan make:middleware AdminAuth
php artisan make:middleware SuperAdminAuth

In Admin Auth file (app/Http/Middleware/AdminAuth.php), import the Facades Auth and edit the handle function:

import Illuminate\Support\Facades\Auth
public function handle($request, Closure $next)
{
    if (Auth::guard('api')->check() && $request->user()->type >= 1) {
        return $next($request);
    } else {
        $message = ["message" => "Permission Denied"];
        return response($message, 401);
    }
}

Same with the Super Admin Auth file (app/Http/Middleware/SuperAdminAuth.php), import the Facades Auth and modify the handle function.

import Illuminate\Support\Facades\Auth
public function handle($request, Closure $next)
{
    if (Auth::guard('api')->check() && $request->user()->type >= 2) {
        return $next($request);
    } else {
        $message = ["message" => "Permission Denied"];
        return response($message, 401);
    }
}

To use our new middleware, add the following lines to the $routeMiddleware array in the kernel file:. (app/Http/Kernel.php)

'api.admin' => \App\Http\Middleware\AdminAuth::class,
'api.superAdmin' => \App\Http\Middleware\SuperAdminAuth::class,

Finally, you can use the middleware in route by using the syntax:

Route::post('route','Controller@method')->middleware('<middleware-name-here>');

<middleware-name-here> in this case can be api.admin, api.superAdmin, etc., as appropriate.

Before the testing, make sure Config Auth file (config/auth.php) run the passport migration. Set the [‘guards‘][‘api‘][‘driver‘] set to passport.

'guards' => [
    'web' => [
        'driver' => 'session', 
        'provider' => 'users', 
    ], 

    'api' => [ 
        'driver' => 'passport', 
        'provider' => 'users', 
    ], 
],

Testing Laravel Authentication and Access Control (Create dummy route for testing)

Access the controller that you want to test. Example, the User Controller (app/Http/Controllers/UserController.php) adds the index function.

public function index()
{
    $response = ['message' => 'Success access!'];
    return response($response, 200);
}

And register the route to the API.

Route::middleware('auth:api')->group(function () {
   Route::get('/users', 'UserController@index')->name('articles');
});

Testing Laravel Authentication and Access Control (Failed)

Accessing the route without an authentication token. Will display the following message below:

{
    "message": "Unauthenticated."
}

Testing Laravel Authentication and Access Control (Success)

Access the login API and use the token to check the access control. It will display this message.

{
    "message": "Success access!"
}