8 min read

Build a secure API using Laravel Passport

Do you want to build a secure API that uses Laravel Passport for its OAuth2 server implementation? This blog article will show you step by step how you could build a secure API resource using Laravel Passport.

Laravel Passport is an OAuth2 server implementation for API authentication using Laravel. If you have ever worked with Google account, Spotify or Dropbox, you might have seen an authentication system before using it. That’s happens with the help of OAuth. Laravel Passport is used to secure your API through different ways of authentication.

When you want to make use of Laravel Passport, you need to install it through Composer, where you need to perform the following command from the root of your project directory:

composer require laravel/passport

When using Laravel Passport, you will interact with the database because the clients and access tokens that you will be retrieving need to be stored somewhere. You might think that nothing needs to be migrated since there are no new migrations, but the migrations of Laravel Passport are included in the composer package.

php artisan migrate

In order to run Laravel Passport, we need to make sure that we run the following command to generate client information for us.

php artisan passport:install

This will create a Client Id and Client Secret. Quick note; you will be using both of these, so make sure that you save them.

Client ID: 1
Client secret: 8yvyFECEuIVvZAJPmMGsG9EoZvgLfJKNMkaYKcFb

In order to find out which token and scope belongs to which user, we need to make sure that we connect with Laravel Passport in the correct way.

We will be using the User model for this example, where you will see that the HasApiTokens trait has been implemented. This is not the right HasApiTokens trait, since we need ot use the trait from Laravel Passport.

+ use Laravel\Passport\HasApiTokens;
- use Laravel\Sanctum\HasApiTokens;

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

The next step is to tell our /Providers/AuthServiceProvider.php to revoke the access tokens, clients, and personal access tokens.

To revoke the right items, we need to call the Passport routes inside the boot method of the /Providers/AuthServiceProvider.php.

use Laravel\Passport\Passport;

public function boot()
{
    $this->registerPolicies();
    Passport::routes();
}

The last step is to set up Laravel Passport as a driver for our API Authentication. This can be done inside the /bookstore/config/Auth.php file, where you need to replace ’driver’ => ‘token’ to:

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

Accessing Tokens

Instead of using the /routes/web.php file, we are going to use the /routes/api.php file to define a new route for our API.

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

With the addition of the route above, we are able to perform a GET request to http://127.0.0.1:8000/api/user.

If you try to perform the request right now, you will receive an error message. Before you can use your endpoint successfully, you have to pass in the right data inside the headers tab of Postman. We got to set the KEY equal to Accept and the VALUE equal to application/json.

If you perform your request now, you will see that the status code is 401, meaning that we are unauthorized to perform this action. If the output was the actual user, we would have got a huge problem. At the moment, the middleware is telling us that the authentication works properly, but the request has been performed by an unauthorized person.

To prevent the 401, we need to generate a unique access token that will allows us to perform requests which are using the middleware.

There are lots of different ways how you could fix this, and I want to use the two keys we generated in the previous section.

The first step is to perform a POST request to the http://127.0.0.1:8000/oauth/token route, which has been added through Laravel Passport.

Inside Postman, you will find a body tab which allows you to pass in data inside the body of your request. The following data needs to be added as key-value pairs.

grant_type:password
client_id: 1
client_secret: 8yvyFECEuIVvZAJPmMGsG9EoZvgLfJKNMkaYKcFb
username: [USERNAME FROM USERS TABLE]
password: [PASSWORD FROM USER]
scope:

Once you save it and perform the request, you will see that the return status code is 200 and we got an access token and refresh token.

If we perform the request one more time, you will see that the values are different this time.

Copy the access token, navigate to the Headers tab and create a new KEY VALUE pair.

The KEY will be Authorization, where the value will be Bearer [CODE].

If we perform a GET request to http://127.0.0.1:8000/api/v1/user, you will see all users available in the database.

Building Resources

To create Authors for a bookstore, we have to create an Author model, database migration, factory, and controller. Let’s perform the following command to create all the mentioned classes.

php artisan make:model -a -r Author

Inside the /migrations/create_authors_table.php migration, you need to replace the up() method with the following code:

public function up()
{
    Schema::create('authors', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
}

Since we are going to perform mass assignment to our model, we need to sure add the column name inside the $fillable property of our Author model.

protected $fillable ['name'];

Let’s migrate our database to create the authors table:

php artisan migrate

The next step is adding dummy data inside our database to create fake users! Let’s open the /factories/AuthorFactory.php and replace the definition() method with the following:

public function definition()
{
    return [
        'name' => $this->faker->name
    ];
}

To use our factory, we got to make sure that we have got our database seeder ready.

php artisan make:seeder AuthorsTableSeeder

Open the /seeders/AuthorsTableSeeder.php file and replace the run() method with the following code:

public function run()
{
    $this->call(AuthorsTableSeeder::class);
}

Don’t forget to replace the model inside the /database/seeder/DatabaseSeeder.php file before you run your seeders.

public function run()
{
     \App\Models\Author::factory(10)->create();
}

Right now, we can migrate and seed our database.

php artisan migrate –seed

We need to make sure that we define the right endpoints for our authors and books endpoint inside the /routes/api.php file.

Route::middleware('auth:api')->prefix('v1')->group(function() {
    Route::get('/user', function(Request $request){
        return $request->user();
    });

    Route::apiResource('/authors', AuthorsController::class);
    Route::apiResource('/books', BooksController::class);
});

AuthorsController

<?php

namespace App\Http\Controllers;

use App\Models\Author;
use Illuminate\Http\Request;
use App\Http\Resources\AuthorsResource;
use App\Http\Requests\AuthorsRequest;

class AuthorsController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return AuthorsResource::collection(Author::all());
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(AuthorsRequest $request)
    {
        $faker = \Faker\Factory::create(1);

        $author = Author::create([
            'name' => $faker->name
        ]);

        return new AuthorsResource($author);
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Author  $author
     * @return \Illuminate\Http\Response
     */
    public function show(Author $author)
    {
        return new AuthorsResource($author);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Author  $author
     * @return \Illuminate\Http\Response
     */
    public function update(AuthorsRequest $request, Author $author)
    {
        $author->update([
            'name' => $request->input('name')
        ]);

        return new AuthorsResource($author);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Author  $author
     * @return \Illuminate\Http\Response
     */
    public function destroy(Author $author)
    {
        $author->delete();
        return response(null, 204);
    }
}

BooksController

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Illuminate\Http\Request;
use App\Http\Resources\BooksResource;

class BooksController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return BooksResource::collection(Book::all());
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $faker = \Faker\Factory::create(1);

        $book = Book::create([
            'name' => $faker->name,
            'description' => $faker->sentence,
            'publication_year' => $faker->year
        ]);

        return new BooksResource($book);
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Book  $book
     * @return \Illuminate\Http\Response
     */
    public function show(Book $book)
    {
        return new BooksResource($book);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Book  $book
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Book $book)
    {
        $book->update([
            'name' => $request->input('name'),
            'description' => $request->input('description'),
            'publication_year' => $request->input('publication_year')            
        ]);

        return new BooksResource($book);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Book  $book
     * @return \Illuminate\Http\Response
     */
    public function destroy(Book $book)
    {
        $book->delete();
        return response(null, 204);
    }
}

AuthorsResource

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class AuthorsResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => (string)$this->id,
            'type' => 'Authors',
            'attributes' => [
                'name' => $this->name,
                'created_at' => $this->created_at,
                'updated_at' => $this->updated_at
            ]
        ];
    }
}

Conclusion 🚀

I hope that this article made Laravel Passport and APIs a lot more clearer for you.

If you did enjoy reading this post and you want me to create an article on a topic you like, please send an email to info@darynazar.com.

The source code of this article can be found right here.