When it comes to building RESTful APIs, PHP’s open source Laravel framework is a leading choice. This is one of the reasons why Laravel remains a top 5 backend framework for web development. Laravel also makes testing a breeze by providing an easy-to-use testing suite to help you test your API endpoints. In this post, we will build a token-based authentication API with Laravel, write tests for our endpoints, and automate the build and testing process with CircleCI.

Prerequisites

To follow along with this post, you will need a few things:

  • PHP >= 7.1 installed on your system (you can confirm that your version in high enough by running the command php -v on your terminal)
  • Composer installed globally (confirm this by running the composer command on your terminal)
  • Git installed on your system
  • A GitHub account
  • A CircleCI account

Once you have these up and running, you will be ready to follow along with the tutorial.

Scaffolding the Laravel API

We will be scaffolding a Laravel 5.6 application. This version of Laravel is supported by PHP >= 7.1 which is the minimum PHP version specified for the tutorial. First, create a new Laravel project by running the following command on your terminal:

composer create-project --prefer-dist laravel/laravel my-laravel-api "5.6.*"

This will create a project with the name my-laravel-api in the location where the command was run. Feel free to give your project your preferred name. Once the scaffolding process is completed, it’s time to initialize the project as a Git repository and link it up with GitHub. Go into your project’s root and run the following command:

git init

Then run the following commands to make your first commit.

git add .
git commit -m “Initial Commit”

Next, create a GitHub repository for your project and push your code to it. I have created a repository with the same name as my project on my GitHub account and will be pushing using the following commands:

git remote add origin https://github.com/coderonfleek/my-laravel-api.git
git push -u origin master

These commands will register the remote repository and push the code to its master branch. Once this process is completed, you will have your local repository linked to your remote repository on GitHub.

Setting up the database

For this tutorial, SQLite will be used for our testing and main databases. Oftentimes, the main database is a more sophisticated DBMS like MySQL or MSSQL, but we will keep things in this post simple, for the sake of demonstration. We will keep the configuration for our main database in our .env file, and then create a .env.testing file to hold our test database configuration.

You already have the .env file which comes with the default project scaffolding. Create a new file named .env.testing and copy the contents of .env.example (which is also created by default) into it.

Replace the database configuration section of both files (.env and .env.testing) with the following configuration:

DB_CONNECTION=sqlite
DB_HOST=null
DB_PORT=null
DB_DATABASE=database/database.sqlite
DB_USERNAME=null
DB_PASSWORD=null

We are using the same configuration for both files because we are using SQLite databases for both our main and testing databases. In the above code snippet, we have set our connection to sqlite and our database to database.sqlite which is contained in our database folder. We have yet to create this database.sqlite file, so go into the database folder at the root of your project and create this file.

The final task in setting up our database is to use one of Laravel’s helpers to point to our SQLite database file in our database configuration file in config/database.php. Inside the config/database.php file, replace the SQLite configuration in the connections array with the one below:

'sqlite' => [
            'driver' => 'sqlite',
            'database' => database_path('database.sqlite'),
            'prefix' => '',
        ]

This will ensure that our SQLite configuration always points to the correct database path if we ever change the environment in which our application is running.

To confirm that all is working perfectly, let’s migrate our database by running the following command:

php artisan migrate

A successful migration should display something similar to the screenshot below:

Make sure that you ignore the database file by declaring it in your .gitignore file.

Setting up token-based authentication with Passport

Our next task is to set up our API project to use token-based authentication. We will be doing that using the Laravel Passport OAuth library which provides a full OAuth2 server implementation for your Laravel application. First, let’s install the laravel/passport package:

composer require laravel/passport v7.5.1

Once the installation is done, run the following command to run the migrations that come with laravel/passport:

php artisan migrate

This should print a screen similar to the one below on your command line:

Passport requires encryption keys to generate access tokens, these keys need to be generated and saved in the database. To generate these keys, run the following command:

php artisan passport:install

After running this command successfully, you should see your client secrets generated.

The next step is to add the Laravel\Passport\HasApiTokens trait to our App\User model. This will introduce helper methods from the laravel/passport package into the application to help with inspecting a user’s token and scopes. Open the app/User.php file and replace its contents with the code below:

<?php

namespace App;

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

class User extends Authenticatable
{
    use Notifiable, HasApiTokens;

    
    protected $fillable = [
        'name', 'email', 'password',
    ];

   
    protected $hidden = [
        'password', 'remember_token',
    ];
}

In the above file, we imported the Laravel\Passport\HasApiTokens trait and instructed our class to use it.

Next, you need to call the Passport::routes method inside the boot method of your AuthServiceProvider contained inside the app/Providers/AuthServiceProvider.php file. Open this file and replace the contents with the code below:

<?php

namespace App\Providers;

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

class AuthServiceProvider extends ServiceProvider
{
    
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }
}

The final configuration that Passport requires is setting it as the driver option in the API authentication guard inside config/auth.php.

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

With this, the application will now use Passport’s TokenGuard to authenticate incoming requests.

Creating the API endpoints

We are going to be building a simple user profile API. The user will be able to do the following:

  • Sign up for a new account
  • Log in to their accounts using their login credentials
  • Fetch their profile
  • Log out of the application

We need to create API endpoints for these four tasks. Go into the file routes/api.php and replace its contents with the code below:

<?php

use Illuminate\Http\Request;

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

In the above code, we create an auth route group and create four routes for the four tasks we stated earlier.

The auth/logout and auth/user routes are authenticated with the auth:api middleware to ensure that only authenticated access is allowed to these endpoints. We also referenced AuthController above which is the controller that we will be creating next. Before that, delete the app/Http/Controllers/Auth folder that comes by default with the project scaffolding. Then, run the following command to create the controller:

php artisan make:controller AuthController

Now place the following code into the newly created controller:

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use App\User;

class AuthController extends Controller
{
    /**
     * Create user
     *
     * @param  [string] name
     * @param  [string] email
     * @param  [string] password
     * @param  [string] password_confirmation
     * @return [string] message
     */
    public function signup(Request $request)
    {
        $request->validate([
            'name' => 'required|string',
            'email' => 'required|string|email|unique:users',
            'password' => 'required|string|confirmed'
        ]);        
        
        $user = new User([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password)
        ]);        
        
        $user->save();        
        
        return response()->json([
            'message' => 'Successfully created user!'
        ], 201);
    }
  
    /**
     * Login user and create token
     *
     * @param  [string] email
     * @param  [string] password
     * @param  [boolean] remember_me
     * @return [string] access_token
     * @return [string] token_type
     * @return [string] expires_at
     */
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|string|email',
            'password' => 'required|string',
            'remember_me' => 'boolean'
        ]);

        $credentials = request(['email', 'password']);        
        if(!Auth::attempt($credentials)){
            return response()->json([
                'message' => 'Unauthorized'
            ], 401);
        }
                                
        $user = $request->user();        
        $tokenResult = $user->createToken('Personal Access Token');
        $token = $tokenResult->token;        
        
        if ($request->remember_me){
            $token->expires_at = Carbon::now()->addWeeks(1); 
        }
                   
        $token->save();        
        
        return response()->json([
            'access_token' => $tokenResult->accessToken,
            'token_type' => 'Bearer',
            'expires_at' => Carbon::parse(
                $tokenResult->token->expires_at
            )->toDateTimeString()
        ]);
    }
  
    /**
     * Logout user (Revoke the token)
     *
     * @return [string] message
     */
    public function logout(Request $request)
    {
        $request->user()->token()->revoke();        
        
        return response()->json([
            'message' => 'Successfully logged out'
        ]);
    }
  
    /**
     * Get the authenticated User
     *
     * @return [json] user object
     */
    public function user(Request $request)
    {
        return response()->json($request->user());
    }
}

Whoa! That is a lot of code. Let’s go through the code method by method.

The signup method

This method receives an email, a password, and the user’s name as parameters, it then creates a user using the Lucid model’s save() method on the User model and returns a success message along with a status of 201.

The login method

This method receives the user email and password and an optional remember_me parameter. It then verifies the credentials, and upon successful verification, returns the access token, token type, and the time that the token will expire.

The logout method

This method receives and revokes the access token given to the user then sends a success message upon log out.

The user method

This method receives the user’s access token and uses it to return the user’s details.

Note: If you see a file .rnd created in your project, ignore this file by adding it to your .gitignore file.i>

Testing the API endpoints

It’s now time to write some tests for our API endpoints. Let’s create a test by running the following command:

php artisan make:test UserTest

This creates the UserTest test file inside the tests/Feature folder. Delete the ExampleTest.php file that comes by default in the folder. Open the UserTest.php file just created and replace its contents with the code below:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

use App\User;

class UserTest extends TestCase
{
   
    use WithFaker;

    private $password = "mypassword";
    
    public function testUserCreation()
    {
       	
       	$name = $this->faker->name();
       	$email = $this->faker->email();

        $response = $this->postJson('/api/auth/signup', [
            'name' => $name, 
            'email' => $email,
            'password' => $this->password, 
            'password_confirmation' => $this->password
        ]); 


        $response
            ->assertStatus(201)
            ->assertExactJson([
                'message' => "Successfully created user!",
            ]);
    }//testUserCreation
    
}

The file above contains a single test that tests our /api/auth/signup API endpoint to ensure that users are able to sign up successfully. The test generates a random email address and user’s name using the already-installed PHP Faker library along with a generic password to create a new user account. The test asserts an HTTP status of 201 (created) and the response message as defined in our controller.

Running tests locally

Now to take our test for a spin. We will be using PHPUnit to run our test. An installation of PHPUnit comes with the phpunit CLI command. You can have this command running globally with a global installation of PHPUnit, or have it at project level. The default scaffolded Laravel project already has the package installed locally which enables us to run our tests.

To run the tests, run the following command at the project root:

./vendor/bin/phpunit

This runs the phpunit command using our local installation of the package. If everything goes fine, all your tests should run successfully and you should have a screen similar to the one below.

Automating our Tests with CircleCI

Time to introduce the power of CI/CD into our Laravel API. We will be creating a pipeline that ensures that once we push new code, our tests automatically run and we get either a successful or failed CI/CD pipeline status. We are going to be using CircleCI to achieve this. Our pipeline will consist of the following steps to enable us to achieve our aim:

  • Spinning up the required environment
  • Installing dependencies with composer
  • Setting up a .env environment file for the project
  • Setting up our test database and running our migrations
  • Generating encryption keys for the laravel/passport package
  • Running our tests

To create a CI/CD pipeline, we need to write a configuration file that CircleCI will use for setting up the pipeline. Go into the root of your project and create a folder named .circleci. Inside this folder, create a file named config.yml.

Spinning up the required environment

In this step, we will be pulling the circleci/php:7.4-node-browsers Docker image from the CircleCI image registry. This image contains PHP 7.4 installed alongside Node and browsers. We then update our environment and install the necessary packages.

Begin the configuration file with the code below:

version: 2
jobs:
  build:
    docker:
      # Specify the version you desire here
      - image: circleci/php:7.4-node-browsers

    steps:
      - checkout

      - run:
          name: "Prepare Environment"
          command: |
            sudo apt update
            sudo docker-php-ext-install zip

Installing dependencies with Composer

The next step is to checkout our code from our repository into the environment that has been set up and install our project dependencies. We also cache our dependencies to increase the speed of our pipeline on subsequent runs.

# Download and cache dependencies
      - restore_cache:
          keys:
            # "composer.lock" can be used if it is committed to the repo
            - v1-dependencies-{{ checksum "composer.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run:
          name: "Install Dependencies"
          command: composer install -n --prefer-dist

      - save_cache:
          key: v1-dependencies-{{ checksum "composer.json" }}
          paths:
            - ./vendor

Setting up a .env environment file for the project

Our Laravel project requires an environment file to run, the .env file. This file is a sensitive file which is why it is ignored in our .gitignore file. This is the reason why we created another version of the file .env.testing to store the credentials we need for running our tests. Unlike .env, .env.testing is pushed to our repo as it does not contain sensitive information.

We will now rename our .env.testing to .env and generate an application key for the Laravel application.

      - run:
          name: "Create Environment file and generate app key"
          command: |
            mv .env.testing .env
            php artisan key:generate

Setting up our test database and running our migrations

The next task is to set up the test database and run our migrations, do that by adding the following step:

      - run:
          name: "Create database and run migration"
          command: |
            touch database/database.sqlite
            php artisan migrate --env=testing

Generating encryption keys for the laravel/passport package

Next, we generate the encryption keys needed by Passport to generate access tokens.

      - run:
          name: "Generate Passport encryption keys"
          command: php artisan passport:install

Running our tests

Finally, with our environment and project fully set up, we can now run our tests using the local installation of the phpunit package.

      - run:
          name: "Run Tests"
          command: ./vendor/bin/phpunit

Full config

Awesome. Now we have our configuration file setup. Below is the full config.yml file:

# PHP CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-php/ for more details
#
version: 2
jobs:
  build:
    docker:
      # Specify the version you desire here
      - image: circleci/php:7.4-node-browsers

    steps:
      - checkout

      - run:
          name: "Prepare Environment"
          command: |
            sudo apt update
            sudo docker-php-ext-install zip

      # Download and cache dependencies
      - restore_cache:
          keys:
            # "composer.lock" can be used if it is committed to the repo
            - v1-dependencies-{{ checksum "composer.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run:
          name: "Install Dependencies"
          command: composer install -n --prefer-dist

      - save_cache:
          key: v1-dependencies-{{ checksum "composer.json" }}
          paths:
            - ./vendor

      # prepare the database
      - run:
          name: "Create Environment file and generate app key"
          command: |
            mv .env.testing .env
            php artisan key:generate

      - run:
          name: "Create database and run migration"
          command: |
            touch database/database.sqlite
            php artisan migrate --env=testing

      - run:
          name: "Generate Passport encryption keys"
          command: php artisan passport:install

      # run tests with phpunit
      - run:
          name: "Run Tests"
          command: ./vendor/bin/phpunit

Commit all your changes and push to the project’s remote repository.

Connecting the API project to CircleCI

Now its time to hand over this configuration to CircleCI to set up our pipeline. We do that by connecting our project to CircleCI. Head over to your CircleCI dashboard and add the project in the Add Project section.

Next to your project, click Set Up Project. This will bring you to a page similar to the one below.

Click Start building to begin building your project. CircleCI will begin to run your pipeline configuration. If you have followed the instructions correctly, you should have a successful build indicated by the screen below.

Click into this workflow and scroll down to the Run Tests section to see the results of your test.

Perfect. We have been able to successfully power our Laravel API with a CI/CD pipeline that automatically runs our test using CircleCI.

Let’s add one more test to our UserTest.php file to test the /api/auth/login endpoint and push our code to observe how CircleCI automatically runs our newly added test. Add the following test below the testUserCreation test.

public function testUserLogin()
{
    $name = $this->faker->name();
   	$email = $this->faker->email();

    $user = new User([
        'name' => $name,
        'email' => $email,
        'password' => bcrypt($this->password)
    ]);        
    
    $user->save();     
    
    $response = $this->postJson('/api/auth/login', [
        'email' => $email,
        'password' => $this->password
    ]);

        
    $response->assertStatus(200);
    $this->assertAuthenticated();
}

Now save the file, commit it, and push your changes to GitHub. CircleCI will once again run the pipeline with the tests, including the one we just added.

Conclusion

In this article, we developed a token-based authentication API with Laravel, tested it, and automated the building and testing process using a CI/CD pipeline created for and run by CircleCI. This is just the starting point for bringing automation into the development of our Laravel APIs. There is a lot more we can do with CircleCI to create a more robust and complex pipeline that takes away a lot of the manual tasks involved in application development.

I hope you have learned something valuable from this post. Happy Coding :)


Fikayo is a fullstack developer and author with over a decade of experience developing web and mobile solutions. He is currently the Software Lead at Tech Specialist Consulting and develops courses for Packt and Udemy. He has a strong passion for teaching and hopes to become a full-time author.