• 8 min read
Build a secure API 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.