Create REST API with authentication using Laravel Passport

In this post, we are going to dig into REST API building using Laravel 5.4 and Passport OAuth2 server provided by laravel. This is the next part of my post Advance interactive database seeding in Laravel post, we left off at migration and seeding of the database. Let’s build the API which will be consumed by our vue.js front end later.

What is OAuth 2

OAuth 2 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service, such as Facebook, GitHub etc. It works by delegating user authentication to the service that hosts the user account and authorizing third-party applications to access the user account. OAuth 2 provides authorization flows for web and desktop applications, and mobile devices.

Laravel Passport

Laravel makes API authentication a breeze using Laravel Passport, which provides a full OAuth2 server implementation for your Laravel application in a matter of minutes.

Setup Laravel Passport

Open up the terminal and pull the passport using composer require laravel/passport . Now register it in app service providers array under config/app.php Laravel\Passport\PassportServiceProvider::class.

Next, run these two commands php artisan migrate and php artisan passport:install , this will migrate the required database table and set up a Personal access client so you can access the API.

After that let’s add Laravel\Passport\HasApiTokens traits on our User model.

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

Next Open app/Providers/AuthServiceProvider.php and add Passport::routes method within the boot method. This method will register the routes necessary to issue access tokens and revoke access tokens, clients, and personal access tokens:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();
}

One last thing you need to change the API guard driver in config/auth.php to use the passport.

'guards' => [
    ...

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

That’s all needed to setup passport in your laravel application. Laravel Passport also provides some handy Vue component to create and manage OAuth clients, to use it just publish the vendor resources using php artisan vendor:publish --tag=passport-components , it will place all component in resources/assets/js/components/passport folder. You need to register in your app.js to use it.

Vue.component(
    'passport-clients',
    require('./components/passport/Clients.vue')
);

Vue.component(
    'passport-authorized-clients',
    require('./components/passport/AuthorizedClients.vue')
);

Vue.component(
    'passport-personal-access-tokens',
    require('./components/passport/PersonalAccessTokens.vue')
);

After registering run npm run dev to compile the js and you can use the component anywhere in your app. For now, just add these components in resources/views/home.blade.php file.

<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>

Go ahead and register a user to log in, you will need to generate a Personal Access Token to access API. Click Create New Token and give a name and copy the generated token. We will use this to make requests to API using Postman.

API Routes

Now we need to define routes for API resources, open the routes/api.php and add below routes.

Route::resource(
    'videos', 'VideoController',
    [ 'except' => ['create', 'edit'] ]
);

Route::resource(
    'channels', 'ChannelController',
    [ 'except' => ['create', 'edit'] ]
);

Route::resource(
    'comments', 'CommentController',
    [ 'except' => ['create', 'edit'] ]
);

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

I am excluding create and edit route from the resource, we don’t need them in our API since they generally used to show the form for creating and edit a particular resource.

Create Resource Controllers

Now we need our resource controllers, let’s generate all of them using

php artisan make:controller VideoController --resource

VideoController

In video controller we will be handling video crud operation with validation and access control, we don’t want some to delete or edit other users video.

use App\Video;
use App\API\ApiHelper;
use App\Repos\Repository;
use Illuminate\Http\Request;

class VideoController extends Controller
{
    use ApiHelper;

    /**
     * @var Repository
     */
    protected $model;

    public function __construct(Video $video)
    {
        $this->model = new Repository( $video );

        // Protect all except reading
        $this->middleware('auth:api', ['except' => ['index', 'show'] ]);
    }

    public function index()
    {
        return $this->model->with('user')->latest()->paginate();
    }

    public function store(Request $request)
    {
        // run the validation
        $this->beforeCreate($request);

        // validate the channel id belongs to user
        if( ! $request->user()->channels()->find($request->get('channel_id', 0)) ) {
            return $this->errorForbidden('You can only add video in your channel.');
        }

        return $request->user()->videos()
                        ->create(
                            $request->only($this->model->getModel()->fillable)
                        );
    }

    public function show($id)
    {
        return $this->model->with('user')->findOrFail($id);
    }

    public function update(Request $request, $id)
    {
        $this->beforeUpdate($request);

        if (! $this->model->update($request->only($this->model->getModel()->fillable), $id) ) {
            return $this->errorBadRequest('Unable to update.');
        }

        return $this->model->find($id);
    }

    public function destroy($id, Request $request)
    {
        // run before delete checks
        if (! $request->user()->videos()->find($id)) {
            return $this->errorNotFound('Video not found.');
        }

        return $this->model->delete($id) ? $this->noContent() : $this->errorBadRequest();
    }
}

Let me explain what’s going on. In construct of controller we are Initialising one field $this->model = new Repository( $video ); which is assigning the Video model using repository, in the second line we are running a middleware on all methods except index and show  so anyone logged in or not can access the data, but when someone wants to change the data like update, store or delete we need to authenticate user first.

index() method

Now index method is going to list a paginated list of latest videos, and we are eager loading the user.

store() method

In store method we first run $this->beforeCreate($request) , it’s just a simple method which is defined in parent Controller runs validation, I kept it in a method so you can always override this to suit your need, but in our API it’s enough. here is the beforeCreate() implementation detail.

public function beforeCreate($request)
{
   // run the validation
   $this->validate( $request, $this->model->getModel()->getValidationRules());
}

As you can see its running validation against the request and validation rules are defined on Video model itself.

/**
 * Validation rules
 *
 * @param bool $forUpdate
 * @return array
 */
public function getValidationRules($forUpdate = false)
{
    $createRule = [
        'title' => 'required|max:200',
        'description' => 'required|min:10',
        'allow_comments' => 'boolean',
        'url' => 'required|url',
        'thumbnail' => 'required|url',
        'channel_id' => 'required|integer'
    ];

    $updateRule = [
        'title' => 'max:200',
        'description' => 'min:10',
        'url' => 'url',
        'thumbnail' => 'url'
    ];

    return $forUpdate ? $updateRule : $createRule;
}

After validation, we are checking that user owns the channel in which it’s trying to add a video.

// validate the channel id belongs to user
if( ! $request->user()->channels()->find($request->get('channel_id', 0)) ) {
    return $this->errorForbidden('You can only add video in your channel.');
}

Once all is ok we are getting all the fillable fields from $request->only($this->model->getModel()->fillable) and saving the new video on user channel.

You can find all the Model relations defined on Git repository.

We used ApiHelper trait has which give adds helper methods errorForbidden(), errorNotFound(),  errorForbidden() etc to our controller.

show() method

Show is simple, it returns a single video with an eager loaded user.

update() method

Similar to store method, it runs the beforeUpdate() which triggers the validation with update rules defined on Video model and returns updated video.

destroy() method

Destroying any data is always simple, it just retrieves the video using user has many relation and deletes it.

You probably thinking what’s the use of $this->model = new Repository( $video ); in  the next section we are going to cover it.

Repository to Access Data

I have created a base repo which takes an Eloquent Model and gives us some methods to interact, it add a layer on top of database access, so if for any reason you want to change the underlying implementation you can easily do so using this wrapper.

class Repository
{
    protected $model;

    /**
     * Repository constructor.
     *
     * @param $model
     */
    public function __construct( Model $model )
    {
        $this->model = $model;
    }

    /**
     * Get all records
     *
     * @param array $columns
     * @return mixed
     */
    public function all($columns = ['*'])
    {
        return $this->model->all($columns);
    }

    /**
     * Get Paginated records
     *
     * @param null $limit
     * @param array $columns
     * @return mixed
     */
    public function paginate($limit = null, $columns = ['*'])
    {
        return $this->model->paginate($limit, $columns);
    }

    /**
     * Find record by id
     *
     * @param $id
     * @param array $columns
     * @return mixed
     */
    public function find($id, $columns = ['*'])
    {
        return $this->model->findOrFail($id, $columns);
    }

    /**
     * Create a new record
     *
     * @param array $attributes
     * @return mixed
     */
    public function create(array $attributes)
    {
        return $this->model->create($attributes);
    }

    /**
     * Update a record
     *
     * @param array $attributes
     * @param $id
     * @return mixed
     */
    public function update(array $attributes, $id)
    {
        $record = $this->find($id);
        return $record->update($attributes);
    }

    /**
     * Delete a record
     *
     * @param $id
     * @return mixed
     */
    public function delete($id)
    {
        return $this->model->destroy($id);
    }

    /**
     * Eager load a relation
     *
     * @param mixed $relations
     * @return mixed
     */
    public function with($relations)
    {
        return $this->model->with($relations);
    }

    /**
     * Get current model instance
     *
     * @return mixed
     */
    public function getModel()
    {
        return $this->model;
    }

    /**
     * Set model to work with
     *
     * @param mixed $model
     * @return Repository
     */
    public function setModel($model)
    {
        $this->model = $model;
        return $this;
    }
}

All methods are commented and easy to understand so I am not explaining it.

Now we have api/videos endpoint complete. Since we have coded using Repository and all the validation are done on parent Controller using defined rules on Model, now you can just copy paste all the controller code and make few changes to complete the API.

You can test the api in postman using our Personal Access Token generated from dashboard. You will be able to list all the videos, channel and comments, but to create, update and delete you must be authenticated.

That’s all in this article, we have our API working with following endpoints.

| GET|HEAD  | api/channels			| List paginated channels
| POST      | api/channels			| Create a channel
| GET|HEAD  | api/channels/{id}		| Get a single channel by id
| PUT|PATCH | api/channels/{id}		| Update a single channel by id
| DELETE    | api/channels/{id}		| Delete a channel by id

| GET|HEAD  | api/comments			| List paginated comments
| POST      | api/comments			| Create a comment
| GET|HEAD  | api/comments/{id}		| Get a single comment by id
| PUT|PATCH | api/comments/{id}		| Update a single comment by id
| DELETE    | api/comments/{id}		| Delete a comment by id

| GET|HEAD  | api/videos			| List paginated videos
| POST      | api/videos			| Create a video
| GET|HEAD  | api/videos/{id}		| Get a single video by id
| PUT|PATCH | api/videos/{id}		| Update a single video by id
| DELETE    | api/videos/{id}		| Delete a video by id

| GET|HEAD  | api/me				| Get current authenticated user details

In next post, I will be making the front end where you will be able to signup and create channel and post video, comment on video etc. using Vue.js. See you in next one, please comment if you have any question or suggestions.

Source Code

  • Napester Shine

    When I tried to run gulp. it says gulpfile not found.

    • Mohd Saqueib

      Gulp is needs node.js, if you dont have it, install it and run in terminal npm install, after this you can run gulp

    • Saqueib

      Make sure you have node.js installed, you should pull the dependencies by running npm install first. then you can run gulp

      • Napester Shine

        I am using laravel 5.4. It doesn’t have any gulpfile by default. I have installed all the dependencies by using npm install. I am getting same error still gulpfile not found.

        • Saqueib

          It seems you don’t have gulp either, follow this and install gulp http://gulpjs.com/ that should fix it

        • Saqueib

          Laravel has switched to npm based build with webpack so you can run npm run dev or npm run watch to keep watching the changes. I have updates the post accordingly.

          • Napester Shine

            Thank you so much, I will give it a try again.

  • Abhishek Choudhery

    thanks man.
    I will try to implement in my existing API’s in Laravel 🙂

    • Saqueib

      let me know how it go.