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

Prerequisites

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

  • PHP >= 8.0.0 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

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

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

Scaffolding the Laravel API

Your first step is to scaffold a new Laravel application. To begin, issue this command:

composer create-project laravel/laravel my-laravel-api-tutorial

This will create a project with the name my-laravel-api-tutorial in the location where the command was run. You can give your project any name you want.

Setting up the database

For this tutorial, you will use SQLite for both the testing and main databases. In practice, you would use a more sophisticated DBMS like MySQL or MSSQL, but we will keep things in this tutorial simple. The configuration for the main database is in the .env file. Create a .env.testing file to hold your test database configuration.

You already have the .env file that 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 this configuration:

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

You are using the same configuration for both files because both your main and testing databases use SQLite. In the previous code snippet, your connection is set to sqlite and your database to database.sqlite, which is in the database folder. You have not yet created this database.sqlite file. Go into the database folder at the root of your project and create it.

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

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

This makes sure that the SQLite configuration always points to the correct database path, even if you change the environment in which the application is running.

To confirm that all is working perfectly, migrate the database by running this command:

php artisan migrate

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

Successfull migration

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

Setting up token-based authentication with Passport

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

composer require laravel/passport --with-all-dependencies

Once the installation is done, run this 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:

Migrate Passport Table

Passport requires encryption keys to generate access tokens, these keys need to be generated and saved in the database. To generate these keys, run this 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 your 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/Models/User.php file and replace its contents with the code below:

<?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',
    ];
}

In this file, you imported the Laravel\Passport\HasApiTokens trait and instructed your class to use it.

Next, open config/auth.php configuration file and define an api authentication guard to set the driver option to passport as shown below:

'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

You are going to be building a simple user profile API. The user will be able to:

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

You need to create API endpoints for these four tasks. Go into the file routes/api.php and add the following code:

<?php

use App\Http\Controllers\AuthController;

Route::group([
    'prefix' => 'auth'
], function () {
    Route::post('login', [AuthController::class, 'login']);
    Route::post('signup', [AuthController::class, 'signup']);

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

This code creates an auth route group and creates four routes for the four tasks 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. You also referenced AuthController above which is the controller that you will be creating next. To do that, run this command to create the controller:

php artisan make:controller AuthController

Now place this code into the newly created controller:

<?php

namespace App\Http\Controllers;

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

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')->accessToken;

        if ($request->remember_me){
            $tokenResult->expires_at = Carbon::now()->addWeeks(1);
        }

        $tokenResult->save();

        return response()->json([
            'access_token' => $tokenResult,
            'token_type' => 'Bearer',
            'expires_at' => Carbon::parse(
                $tokenResult->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());
    }
}

That is a lot of code. I will go through it for you, 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.

Testing the API endpoints

It is now time to write some tests for the API endpoints. Create a test by running this 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\Models\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!",
            ]);
    }

    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();
    }

}

This file contains the test for/api/auth/signup and /api/auth/login API endpoints. These endpoints ensure that users are able to sign up and login successfully. The test generated 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 also asserts that the appropriate HTTP status codes and messages are defined within the controller.

Running tests locally

Now to take your test for a spin. You will be using PHPUnit to run your 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 your tests.

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

php artisan test

This runs the phpunit command using your 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.

PHPUnit test successful

Automating your tests with CircleCI

Time to introduce the power of CI/CD into your Laravel API. You will be creating a pipeline that runs tests automatically every time you push new code. You get either a successful or failed CI/CD pipeline status from CircleCI. Your pipeline will consist of these steps:

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

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

Open the newly created configuration file and use this code for it:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      # Specify the version you desire here
      - image: cimg/php:8.2.7

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

      # 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: "Generate App key"
          command: php artisan key:generate

      - run:
          name: "Install sqlite"
          command: sudo apt-get install libsqlite3-dev

      - 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: php artisan test

In this configuration file, you pulled the Docker image for PHP from the CircleCI image registry. This image contains the PHP programming language, composer, Node and browsers extensions.

Next, it checked out your code, created a .env file and installed the project’s dependencies. After generating the application key with php artisan key:generate, it installed SQLite, ran migrations for the database and finally ran the tests with PHPUnit.

You can now set up a repository on GitHub and push your project to it. Review Pushing a project to GitHub for instructions. In the next section, you will link the project to CircleCI.

Connecting the API project to CircleCI

Now its time to hand over this configuration to CircleCI to set up your pipeline. You do that by connecting your project to CircleCI. Head over to your CircleCI dashboard. If you signed up with your GitHub account, all your repositories will be available on your project’s dashboard.

Click Set Up Project next to your my-laravel-api-tutorial project.

Select project

On the Select your config.yml file screen, select the Fastest option and type main as the branch name. CircleCI will automatically locate the config.yml file. Click Set Up Project to start the workflow.

Select Configuration

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.

Successful Build

Perfect. You have successfully powered your Laravel API with a CI/CD pipeline that automatically runs your test using CircleCI.

Conclusion

In this article, you 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 your Laravel APIs. There is a lot more you 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. The complete source code can be found here on GitHub.

Happy coding!


Fikayo Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.

Read more posts by Fikayo Adepoju