Laravel Passport Tutorial

Getting Started with Laravel Passport: Implementing Authentication

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

Step 2: Adding Table and Model

php artisan make:migration create_users_table --create=users
php artisan make:migration create_roles_table --create=roles
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();
});
Schema::create('roles', function (Blueprint $table) {
  $table->bigincrements('role_id');
  $table->string('role_name')->unique();
  $table->string('role_desc')->nullable();
  $table->timestamps();
});
php artisan make:model User 
php artisan make:model Role
/**
     * The attributes that are mass assignable.
     * @var array<int, string>
     */
    protected $fillable = [
        'first_name',
        'last_name',
        'email',
        'password',
        'role_id',
        'role'
    ];
// 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;
use HasApiTokens, HasFactory, Notifiable;
  • 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.
  • 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.
  • 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.
use Illuminate\Support\Facades\Schema
Schema::defaultStringLength(191);
php artisan migrate

Step 3: Create the Necessary Pieces of Middleware

php artisan make:middleware ForceJsonResponse
public function handle($request, Closure $next)
{
    $request->headers->set('Accept', 'application/json');
    return $next($request);
}
'json.response' => \App\Http\Middleware\ForceJsonResponse::class,
\App\Http\Middleware\ForceJsonResponse::class,

Creating CORS (Cross-origin Resource Sharing)

php artisan make:middleware Cors
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');
}
'cors' => \App\Http\Middleware\Cors::class,
\App\Http\Middleware\Cors::class,
Route::group(['middleware' => ['cors', 'json.response']], function () {
    // ...
});

Step 4: Create User Authentication Controllers for the API

php artisan make:controller Auth/ApiAuthController
use App\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
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);
}
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

{
    "first_name": "First",
    "last_name": "Last",
    "email" : "test@email.com",
    "password" : "password",
    "password_confirmation" : "password"
}
{
    "email" : "1a@gmail.com",
    "password" : "123123123"
}
{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGci...0pZQmPw1WBgzX2N_RNDHlahkkv6xVIwgLE"
}

Accessing Auth Route using token.

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

Step 5: Create Password Reset Functionality

php artisan make:controller ForgotPasswordController --resource
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);
}
php artisan make:controller ResetPasswordController --resource
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);
}
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
php artisan make:notification MailResetPasswordNotification 
<?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 [
            //
        ];
    }
} 
public function sendPasswordResetNotification($token)
{
    $this->notify(new \App\Notifications\MailResetPasswordNotification($token));
}

Step 6: Create Access Control Middleware

php artisan make:migration update_users_table_to_include_type --table=users
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');
    });
}
php artisan migrate
'type' => 'integer',

The Access Control Middleware Itself

php artisan make:middleware AdminAuth
php artisan make:middleware SuperAdminAuth
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);
    }
}
'api.admin' => \App\Http\Middleware\AdminAuth::class,
'api.superAdmin' => \App\Http\Middleware\SuperAdminAuth::class,
Route::post('route','Controller@method')->middleware('<middleware-name-here>');
'guards' => [
    'web' => [
        'driver' => 'session', 
        'provider' => 'users', 
    ], 

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

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

public function index()
{
    $response = ['message' => 'Success access!'];
    return response($response, 200);
}
Route::middleware('auth:api')->group(function () {
   Route::get('/users', 'UserController@index')->name('articles');
});

Testing Laravel Authentication and Access Control (Failed)

{
    "message": "Unauthenticated."
}

Testing Laravel Authentication and Access Control (Success)

{
    "message": "Success access!"
}

Conclusion