Laravel 8 REST API with Passport Authentication

[1] Create a project.

Example:

  • Laravel version=8

  • Project name=lara8passport

[2] Add package laravel/passport.

composer require laravel/passport

Output example:

The package laravel/passport is added to the composer.json file.

[3] Migrate database.

php artisan migrate

Output example:

Notice that some oauth-related tables have been migrated.

[4] Install laravel/passport.

php artisan passport:install

Output example:

Note: Keep the following details in a secure place.

Client ID: 1
Client secret: tXll1sTSr8QCCPGNHXFBrxnIxIJLjVO4DNGtE7xe

Client ID: 2
Client secret: pV7YnUH2K4AcHHjnDbtJan0qRGDsx5HBYIXpXp2T

[5] Update Environment Variables.

(Update the .env file with data from step 4)

PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=TViBskWZuyQA700di3oxhtib7pX1JXpaoHSPuSIy

PASSPORT_PASSWORD_GRANT_CLIENT_ID=2
PASSPORT_PASSWORD_GRANT_CLIENT_SECRET=iTVGhmSIprC26iFmkCyQsosF9xUe8TznpyjcHsZB

[6] Publish Passport configuration.

php artisan vendor:publish --tag=passport-config

Outcome: The file config/passport.php is created.

[7] Edit Passport configuration.

(Update/Add the following lines in config/passport.php)

  • The personal_access_client entry already existed. Check that it tallies with the above data.

  • The password_grant_client entry does not exist yet. Add the entry.

    /*
    |--------------------------------------------------------------------------
    | Personal Access Client
    |--------------------------------------------------------------------------
    */
    'personal_access_client' => [
        'id' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_ID'),
        'secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'),
    ],
    /*
    |--------------------------------------------------------------------------
    | Password Grant Client
    |--------------------------------------------------------------------------
    */
    'password_grant_client' => [
        'id' => env('PASSPORT_PASSWORD_GRANT_CLIENT_ID'),
        'secret' => env('PASSPORT_PASSWORD_GRANT_CLIENT_SECRET'),
    ],

[8] Update User Model.

(Update App/Models/User.php)

  • Remove "use Laravel\Sanctum\HasApiTokens;"

  • Insert "use Laravel\Passport\HasApiTokens;"

<?php

namespace App\Models;

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;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

[9] Update Auth Guard.

(Update config/auth.php)

  • Set 'driver' => 'passport'.
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

[10] Update AuthServiceProvider.

(Update app/Providers/AuthServiceProvider.php.php)

  • Add "use Laravel\Passport\Passport;"

  • Uncomment ModelPolicy.

  • Add Passport::routes() in boot method

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array<class-string, class-string>
     */
    protected $policies = [
        'App\Models\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        if (!$this->app->routesAreCached()) {
            Passport::routes(); 
        }
        Passport::tokensExpireIn(now()->addDays(15));
        Passport::refreshTokensExpireIn(now()->addDays(30));
        Passport::personalAccessTokensExpireIn(now()->addMonths(6));
    }
}

[11] Create AuthController

  • Run Artisan command
php artisan make:controller Api/AuthController
  • Edit codes (in App/Http/Controllers/Api/AuthController.php)
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Laravel\Passport\RefreshTokenRepository;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Request;
use App\Models\User;
use Carbon\Carbon;
class AuthController extends Controller
{

    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email|unique:users',
            'password' => 'required'
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors(), 400);
        }

        $user = new User();
        $user->name = $request->name;
        $user->email = $request->email;
        $user->password = bcrypt($request->password);
        $user->save();
        return response()->json(['data' => $user]);
    }

    public function login(Request $request)
    {
        $credentials = $request->only(['email', 'password']);
        $validator = Validator::make($credentials, [
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors(), 400);
        }

        if (!auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        /* ------------ Create a new personal access token for the user. ------------ */
        $tokenData = auth()->user()->createToken('MyApiToken');
        $token = $tokenData->accessToken;
        $expiration = $tokenData->token->expires_at->diffInSeconds(Carbon::now());

        return response()->json([
            'access_token' => $token,
            'token_type' => 'Bearer',
            'expires_in' => $expiration
        ]);
    }

    public function getUser()
    {
        return response()->json(auth()->user());
    }

    public function logout()
    {
        $token = auth()->user()->token();

        /* --------------------------- revoke access token -------------------------- */
        $token->revoke();
        $token->delete();

        /* -------------------------- revoke refresh token -------------------------- */
        $refreshTokenRepository = app(RefreshTokenRepository::class);
        $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($token->id);

        return response()->json(['message' => 'Logged out successfully']);
    }

    /* ----------------- get both access_token and refresh_token ---------------- */
    public function loginGrant(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors(), 400);
        }

        $baseUrl = url('//localhost');
        $response = Http::post("{$baseUrl}/oauth/token", [
            'username' => $request->email,
            'password' => $request->password,
            'client_id' => config('passport.password_grant_client.id'),
            'client_secret' => config('passport.password_grant_client.secret'),
            'grant_type' => 'password'
        ]);

        $result = json_decode($response->getBody(), true);
        if (!$response->ok()) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        return response()->json($result);
    }

    /* -------------------------- refresh access_token -------------------------- */
    public function refreshToken(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'refresh_token' => 'required'
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors(), 400);
        }

        $baseUrl = url('//localhost');
        $response = Http::post("{$baseUrl}/oauth/token", [
            'refresh_token' => $request->refresh_token,
            'client_id' => config('passport.password_grant_client.id'),
            'client_secret' => config('passport.password_grant_client.secret'),
            'grant_type' => 'refresh_token'
        ]);

        $result = json_decode($response->getBody(), true);
        if (!$response->ok()) {
            return response()->json(['error' => $result['error_description']], 401);
        }
        return response()->json($result);
    }
}

[12] Edit Route Service Provider.

(Edit App/Providers/RouteServiceProvider.php)

  • Uncomment $namespace.
protected $namespace = 'App\\Http\\Controllers';

[13] Update Redirection.

(Edit App/Http/Middleware/Authenticate.php)

  • Update the redirectTo() function.
<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {
        if ($request->is('api/*') || $request->is('oauth/*')) {
            return route('unauthorized');
        }
        if (! $request->expectsJson() ) {
            return route('login');
        }
    }
}

[14] Update Routes.

(Edit in Routes/Api.php)

Define the routes for the following endpoints:

  • register

  • login

  • logout

  • user

  • login_grant

  • refresh

  • unauthorized

<?php

use Illuminate\Support\Facades\Route;

Route::group(['prefix' => 'auth', 'namespace' => 'Api'], function () {

    Route::post('register',     'AuthController@register');

    /* ------------------------ For Personal Access Token ----------------------- */
    Route::post('login',        'AuthController@login');
    /* -------------------------------------------------------------------------- */

    Route::group(['middleware' => 'auth:api'], function () {
        Route::get('logout',    'AuthController@logout');
        Route::get('user',      'AuthController@getUser');
    });

   /* ------------------------ For Password Grant Token ------------------------ */
    Route::post('login_grant',  'AuthController@loginGrant');
    Route::post('refresh',      'AuthController@refreshToken');
    /* -------------------------------------------------------------------------- */

    /* -------------------------------- Fallback -------------------------------- */
    Route::any('{segment}', function () {
        return response()->json([
            'error' => 'Invalid url.'
        ]);
    })->where('segment', '.*');
});

Route::get('unauthorized', function () {
    return response()->json([
        'error' => 'Unauthorized.'
    ], 401);
})->name('unauthorized');

[15] Test Endpoints.

[1] Register

curl -X POST https://mugxo.ciroue.com/api/auth/register `
     -H 'Content-Type: application/x-www-form-urlencoded' `
     -H 'Accept: application/json' `
     -d 'name=saya' `
     -d 'email=saya@gmail.com' `
     -d 'password=Abcd1234'

[2] Login

curl -X POST https://mugxo.ciroue.com/api/auth/login `
     -H 'Content-Type: application/x-www-form-urlencoded' `
     -H 'Accept: application/json' `
     -d 'email=saya@gmail.com' `
     -d 'password=Abcd1234'

[3] User

curl -X GET https://mugxo.ciroue.com/api/auth/user `
     -H 'Content-Type: application/x-www-form-urlencoded' `
     -H 'Authorization: Bearer <access_token>'

[4] Login Grant

curl -X POST https://mugxo.ciroue.com/api/auth/login_grant `
     -H 'Content-Type: application/x-www-form-urlencoded' `
     -H 'Accept: application/json' `
     -d 'email=saya@gmail.com' `
     -d 'password=Abcd1234'

[5] Refresh

curl -X POST https://mugxo.ciroue.com/api/auth/refresh `
     -H 'Content-Type: application/x-www-form-urlencoded' `
     -H 'Accept: application/json' `
     -d 'refresh_token=<refresh_token>'

.