Laravel REST API with JWT and TDD
Create Laravel Project
For the first time, we need to create a laravel project using composer
composer create-project laravel/laravel laravel-rest-api
After complete generating laravel project named “laravel-rest-api”, then go to the folder
cd laravel-rest-api
Then, open the folder using your favorit IDE, for the example using visual studio code
code .
go to terminal/bash, dont forget to generate key for laravel application
php artisan key:generate --ansi
Testing in Laravel Project
search name file “phpunit.xml” in root folder project, open that file in visual studio code, search code contains this line
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
change it to (uncomment it) :
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
Laravel Framework by default has 2 built-in types of tests.
you can see too, in “phpunit.xml” contains code like below
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
that code lines means :
- Unit Test located in folder “tests/Unit”
- Integration Test/Api Test located in folder “tests/Feature”
OK, let’s try looking at the two folders above
In the unit test folder there is a file called ExampleTest.php. If we open it using an editor, the source code contents are as follows :
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
Likewise with the integration test folder. If we open it using an editor, the source code contents are as follows :
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
This means we have two test files, one test file for the unit test, and another file for the integration test.
OK, we will try to run all the tests on these two files. Open terminal in root project folder, then run command like below
.\vendor\bin\phpunit
The result is :
Runtime: PHP 8.2.4
Configuration: D:\projects\laravel-rest-api\phpunit.xml
.. 2 / 2 (100%)
Time: 00:00.193, Memory: 24.00 MB
OK (2 tests, 2 assertions)
from result above, we get conclusion that all test running succesfully.
Installing JWT Package to Laravel Project
In this step, we will install php-open-source-saver/jwt-auth
package. So open the terminal and run the below command:
composer require php-open-source-saver/jwt-auth
And now publish the configuration file by running this command:
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
Now run the below command to generate JWT secret key like:
php artisan jwt:secret
This command will update your .env
file like this:
.env
JWT_SECRET=eIsucILD4FXFjYTC4ahdWTcf4rnXRtJsJT1A1R0OwYY8Fll6kngYqiQ6hzZaydeT
JWT_ALGO=HS256
Configuring Connection to Database
I am going to use the MYSQL database for this jwt auth laravel 10. So connect the database by updating.env like this:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=YOUR_DB_NAME
DB_USERNAME=YOUR_DB_USERNAME
DB_PASSWORD=YOUR_DB_PASSWORD
Now run php artisan migrate
command to migrate the database.
you’ll get message like below :
INFO Preparing database.
Creating migration table ............................................................................................................... 32ms DONE
INFO Running migrations.
2014_10_12_000000_create_users_table ................................................................................................... 42ms DONE
2014_10_12_100000_create_password_reset_tokens_table ................................................................................... 70ms DONE
2019_08_19_000000_create_failed_jobs_table ............................................................................................. 53ms DONE
2019_12_14_000001_create_personal_access_tokens_table .................................................................................. 64ms DONE
Configuring Guard API
Now, we can continue to configuring guard API. let open file config/auth.php
. next, please find code like below :
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
then, change to code like below :
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
In code above, we add new guard rule called api
and inside it we adding driver
with value jwt
and for provider
with value users
Updating User Model
Now all are set to go. Now we have to update the User model like below. So update it to create laravel jwt auth in app\Models\User.php
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
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',
];
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
from code changes above ini User model, first we import an interface of JWT
use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject;
After that, we implement User class with JWTSubject
class User extends Authenticatable implements JWTSubject //<-- implements JWTSubject
{
//...
And last, we added 2 method namely getJWTIdentifier
and getJWTCustomClaims
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
Modifying Authenticate Middleware
Now we have to update the Authenticate middleware like below (app\Middleware\Authenticate.php :
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : (request()->hasHeader('Authorization') ? null : route('login') );
}
}
Creating an API Controller for Auth
Now we have to create AuthController to complete our JWT authentication with a refresh token in Laravel 10. So run the below command to create a controller:
php artisan make:controller API/AuthController
Now update this controller (app/Http/Controllers/API/AuthController.php) like this:
<?php
namespace App\Http\Controllers\API;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
use PHPOpenSourceSaver\JWTAuth\Exceptions\JWTException;
use PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException;
use PHPOpenSourceSaver\JWTAuth\Exceptions\TokenInvalidException;
class AuthController extends Controller
{
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login', 'register']]);
}
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email',
'password' => 'required|string',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$credentials = $request->only('email', 'password');
$token = auth()->guard('api')->attempt($credentials);
if (!$token) {
return response()->json([
'message' => 'Unauthorized',
], 401);
}
$user = auth()->guard('api')->user();
return response()->json([
'user' => $user,
'authorization' => [
'token' => $token,
'type' => 'bearer',
'expires_in' => auth()->guard('api')->factory()->getTTL() * 60
]
],200);
}
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
if(!$user){
return response()->json([
'success' => false,
], 409);
}
return response()->json([
'message' => 'User created successfully',
'user' => $user
],201);
}
public function logout()
{
$removeToken = JWTAuth::invalidate(JWTAuth::getToken());
if($removeToken){
return response()->json([
'message' => 'Successfully logged out',
]);
}else{
return response()->json([
'success' => false,
'message' => 'Failed logged out',
], 409);
}
}
public function refresh()
{
$token = auth()->guard('api')->refresh();
if($token){
return response()->json([
'user' => auth()->guard('api')->user(),
'authorization' => [
'token' => $token,
'type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]
]);
}else{
return response()->json([
'success' => false,
'message' => 'Failed refresh token',
], 409);
}
}
}
Creating a Route for register Auth API Controller
Here, we need to add routes to set laravel generate jwt token and laravel 10 jwt authentication tutorial. So update the api routes file (routes\api.php
) like this :
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\API\AuthController;
Route::controller(AuthController::class)->group(function () {
Route::post('login', 'login');
Route::post('register', 'register');
Route::post('logout', 'logout');
Route::post('refresh', 'refresh');
});
Now if you start your server by running php artisan serve
and test all API via Postman like this:
Register User
POST /api/register
{
"name" : "Muhammad Tri Wibowo",
"email" : "bowo@email.com",
"password" : "password"
}
The result is
{
"message": "User created successfully",
"user": {
"name": "Muhammad Tri Wibowo",
"email": "bowo@email.com",
"updated_at": "2023-10-23T23:53:55.000000Z",
"created_at": "2023-10-23T23:53:55.000000Z",
"id": 2
}
}
Login User to get JWT Token
POST /api/login
{
"email" : "bowo@email.com",
"password" : "password"
}
The result is
{
"user": {
"id": 2,
"name": "Muhammad Tri Wibowo",
"email": "bowo@email.com",
"email_verified_at": null,
"created_at": "2023-10-24T00:46:56.000000Z",
"updated_at": "2023-10-24T00:46:56.000000Z"
},
"authorization": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvYXBpL2xvZ2luIiwiaWF0IjoxNjk4MTA4NzMwLCJleHAiOjE2OTgxMTIzMzAsIm5iZiI6MTY5ODEwODczMCwianRpIjoiY3FyZWFsRUs5cWFvakR3aCIsInN1YiI6IjIiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.qoVgl5NkXmcivGPd8vTdv979Fdv4kXsWO9dBxJPdh74",
"type": "bearer",
"expires_in": 3600
}
}
Refresh Token User to get new JWT Token
POST /api/refresh
Request Headers
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvYXBpL2xvZ2luIiwiaWF0IjoxNjk4MTA4ODc5LCJleHAiOjE2OTgxMTI0NzksIm5iZiI6MTY5ODEwODg3OSwianRpIjoiblduRElXd1lvQUtSTTdQMSIsInN1YiI6IjIiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.I1sb7dGYyDmr4j_wIKSr9_Y6Xj-VxfsFUK244kou1w8
The result is
{
"user": {
"id": 2,
"name": "Muhammad Tri Wibowo",
"email": "bowo@email.com",
"email_verified_at": null,
"created_at": "2023-10-24T00:46:56.000000Z",
"updated_at": "2023-10-24T00:46:56.000000Z"
},
"authorization": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvYXBpL3JlZnJlc2giLCJpYXQiOjE2OTgxMDg4NzksImV4cCI6MTY5ODExMjQ5MywibmJmIjoxNjk4MTA4ODkzLCJqdGkiOiJSNEdTVEl1WVZqWmV1U250Iiwic3ViIjoiMiIsInBydiI6IjIzYmQ1Yzg5NDlmNjAwYWRiMzllNzAxYzQwMDg3MmRiN2E1OTc2ZjcifQ.tDFQQn2DAeSKnkJJlcGl9SjJm8XzVPAKVmPU3AleJ48",
"type": "bearer",
"expires_in": 3600
}
}
Logout User to Invalidate JWT Token
POST /api/refresh
Request Headers
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvYXBpL2xvZ2luIiwiaWF0IjoxNjk4MTA4ODc5LCJleHAiOjE2OTgxMTI0NzksIm5iZiI6MTY5ODEwODg3OSwianRpIjoiblduRElXd1lvQUtSTTdQMSIsInN1YiI6IjIiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.I1sb7dGYyDmr4j_wIKSr9_Y6Xj-VxfsFUK244kou1w8
The result is
{
"message": "Successfully logged out"
}
TDD for API Auth
To create a new test case, use the make:test
Artisan command. By default, tests will be placed in the tests/Feature
directory.
Ok, we will create Unit test and integration test with command below
php artisan make:test AuthTest
The command above will create file test in tests\Feature\AuthTest.php
Next, try replace/update that file with code below
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
class AuthTest extends TestCase
{
const NAME = "User";
const EMAIL = "user@user.com";
const PASSWORD = "password";
/**
* A basic feature test example.
*/
protected function seed_user(): void
{
$userData = [
'name' => self::NAME,
'email' => self::EMAIL,
'password' => Hash::make(self::PASSWORD)
];
User::create($userData);
}
public function test_it_can_register_user(): void
{
$userData = [
'name' => self::NAME,
'email' => self::EMAIL,
'password' => self::PASSWORD
];
$response = $this->json('POST', '/api/register', $userData);
$response->assertStatus(201)
->assertJson(['user'=>['name' => self::NAME, 'email' => self::EMAIL]]);
$this->assertDatabaseHas('users', ['name' => self::NAME, 'email' => self::EMAIL]);
}
public function test_it_can_be_login(): void
{
$this->seed_user();
$userData = [
'email' => self::EMAIL,
'password' => self::PASSWORD
];
$response = $this->json('POST', '/api/login', $userData);
$response
->assertStatus(200)
->assertJsonStructure(['authorization' => [
'token', 'type', 'expires_in'
]]);
}
public function test_it_can_be_logout(): void
{
$this->seed_user();
$user = User::where('email', self::EMAIL)->first();
$token = JWTAuth::fromUser($user);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->json('POST', '/api/logout');
$response->assertStatus(200)
->assertJson(['message' => 'Successfully logged out']);
}
public function test_it_can_be_refresh_token(): void
{
$this->seed_user();
$user = User::where('email', self::EMAIL)->first();
$token = JWTAuth::fromUser($user);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->json('POST', '/api/refresh');
$response
->assertStatus(200)
->assertJsonStructure(['authorization' => [
'token', 'type', 'expires_in'
]]);
}
}
After that, we can test our test files with command below
php artisan test
The result is
PASS Tests\Unit\AuthTest
✓ create user 0.22s
PASS Tests\Unit\ExampleTest
✓ that true is true
PASS Tests\Feature\AuthTest
✓ it can register user 0.06s
✓ it can be login 0.04s
✓ it can be logout 0.03s
✓ it can be refresh token 0.02s
PASS Tests\Feature\ExampleTest
✓ the application returns a successful response 0.03s
Tests: 7 passed (18 assertions)
Duration: 0.51s
Source code for this tutorial is here
Reference :
Quick start — Laravel JWT Auth (laravel-jwt-auth.readthedocs.io)
Laravel 10 JWT — Complete API Authentication Tutorial (laravelia.com)
Testing: Getting Started — Laravel 10.x — The PHP Framework For Web Artisans