Lumen 8 and JWT AUTH for Real World
Part 1/5 — General Setup
Yes! We will use the Lumen Micro PHP Framework with JWT in a real world case with minimum settings for authorization, security, databases and services from development to deployment.
Our goal in this section of the General Setup is to create services that uses JWT Auth for Authentication and to build and modify core applications according to requirements often applied in real projects.
so let’s get started!
LUMEN MICRO FRAMEWORK
Make sure all requirements are met. Install Lumen version 8 on your development / local server, name this project lumen-jwtauth. In the root terminal of the application folder, run the command
$ composer create-project laravel/laravel lumen-jwtauth "8.*"
if not exist, create .env file, copy from (.env.example
) this file will be edited later.
when done, make sure the server is running, run it with the command
$ php -S localhost:8000 -t public
LUMEN ARTISAN
By default Lumen doesn’t have complete artisan features, so we need the Lumen Generator package to get all artisan features. Install the package with commands
$ composer require flipbox/lumen-generator "8.*"
In the file (bootstrap/app.php
) add Service Provider Lumen Generator
...$app->register(Flipbox\LumenGenerator\LumenGeneratorServiceProvider::class);...
LUMEN JWT-AUTH
Lumen JWT-AUTH is the package required for authentication and authorization. Install the package with the command
$ composer require tymon/jwt-auth "1.*"
OK, all dependencies are installed. Next we will do the configuration
Setup Application Core Library
Create a folder and library files for us to use as global application classes and methods (app\Libraries\Core.php
)
- The setResponse() method is used to return a response in json format
- The renderRoute() method is used to read all the route files in the
routes/
subfolder which we will use later - The log() method is used to manually log and provide a return error code reference so that it is easy to find in the logs file.
lets continue by modifying the Controller file
(app\Http\Controllers\Controller.php
) so that all controllers that we will create later can access the Core Library class above and other methods later.
...use app\Libraries\Core
use Laravel\Lumen\Routing\Controller as BaseController;class Controller extends BaseController
{
public $core ; public function __construct(){
/** define Core as global Library */
$this->core = new Core();
}
public function missingMethod(){
return $this->core->setResponse();
}}
Setup CORS Middleware (optional)
You can activate CORS as needed, because there may be some data that is allowed to be accessed via Javascript code from cross-domain calls. This is optional if necessary.
create middleware file for CORS
(app/Http/Middleware/CorsMiddleware.php
) with the artisan command
$ php artisan make:middleware CorsMiddleware
for this example in the settings above 'Access-Control-Allow-Origin' => '*'
By default we allow all domains to be able to access the services that we will create, please change it as needed.
Unit Test
In real world, the Unit Test is an important step that must be done first to define the goal of the service we want to create and this is a must because it will make it easier for us to do internal testing during development before proceeding to the deployment stage to avoid unwanted bugs or anomalies. when the application has been deployed to production.
In the next steps, we will make a unit test before developing a features or bugs, so that it is easier to determine the tasks or goals that we must fulfill so that the features or bugs we are working on can be said to be complete or meet the DOD (Definition Of Done).
First we must edit the phpunit configuration file (phpunit.xml
)
then delete the tests/ExampleTest.php
file
Setup Core Routes & Exception
We will create a Core Route where this route will handle exceptions and routes. Remember the first step before developing features or bugs is creating unit tests. Create a new test file called (tests/EndpointTest.php
) with artisan
$ php artisan make:test EndpointTest
The following is an explanation of the tests that we did above
- test_unknown_path_should_404() when an unknown endpoint for example
/any/random/endpoint
is accessed with the get method, it must be fulfilled (assert) return response json which contains the key ‘error’ and status = 404 - test_get_version() when the endpoint
/v1/version
is accessed with the get method, a return response containing the lumen version of text must be met - test_ping_should_return_pong(), when the endpoint
/v1/ping
is accessed with the get method it must return a response containing the text ‘pong’
now let’s run the test
$ phpunit tests/EndpointTest.php
we get info on 3 failed test stages, calm down this is normal because we haven’t developed a single feature in accordance with the test provisions above. This is the challenge so that we can pass the test so that no more errors occur.
We will modify the default web route file (routes/web.php
) to have a new /v1
prefix versioning endpoint.
You can see that we use the Core Class Library to render all routes in the routes/
folder with the Core::renderRoutes() method.
Next we will create a simple versioning for our API endpoint route, so that all API prefixes will start with the version, for example /v1/…
Versioning api endpoints are very useful for development and production purposes because we can easily switch routes for the same service without changing the original endpoints.
Create a v1/
folder and a common.php file inside the routes/
folder
(routes/v1/common.php
) This route file serves to hold a collection of routes that are commonly used in our application.
Next we setup Exception 404 and 500. Create a default error exception for unknown routes/endpoints. Modify the render() method on file
(app\Exceptions\Handler.php
)
PLEASE REMEMBER that the exception that we create will only be active when the environment mode APP_DEBUG=false
in the file (.env
). This handler function is to minimize the messages that appear due to errors during Production.
...
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use app\Libraries\Core;
...public function render($request, Throwable $exception)
{
/* only run if debug is turn off */
if ( !env('APP_DEBUG', true) ) {
$core = new Core;
/* handling 404 exception */
if($exception instanceof NotFoundHttpException){
return response()->json([
'error' => 'Not Found',
])->setStatusCode(404);
} /* handling 500 exception */
$exception_name = get_class($exception);
$error = $core->log('error', "Exception ($exception_name) : " . $exception->getTraceAsString() , true);
return response()->json([
'error' => "Server problem, code [$error]",
])->setStatusCode(500);
}
return parent::render($request, $exception);
}
if we try to access the wrong endpoint, for example http://localhost:8000/any/random/endpoint
, an error will appear with a status code 404
Well, for this step we have developed all the features or goals that we want to get from the test file (tests/EndpointTest.php
) that we created earlier. Let’s try to run the test again with the same command before
$ phpunit tests/EndpointTest.php
make sure all test pass successfully
if an error occurs please fix it according to the error message :)
The following are the results if we test manually in the browser
At this point, our Route and Exception setup has been completed.
Setup App & JWT AUTH
At this step we will develop JWT AUTH and User Registration. Like the previous step, we will create a unit test to define the DOD we want. Create a unit test file with the name (tests/UserRegistrationTest.php
)
$ php artisan make:test UserRegistrationTest
- test_validate_firstname_properties(): validate the firstname parameter when executing the post
/v1/register
method with an unfulfilled status property require | max: 100 | min: 2 with response json ‘error’ and code = 400 - test_validate_lastname_properties () validate the lastname parameter when executing the post
/v1/register
method with unfulfilled status properties require | max: 100 | min: 2 with response json ‘error’ and code = 400 - test_validate_email_properties() validate email parameters when executing post /v1/register methods with unfulfilled status properties
require | unique: users | email with response json ‘error’ and code = 400 - test_validate_password_properties() validate the password parameter when performing the post
/v1/register
method with unfulfilled status properties require | max: 100 | min: 6 with response json ‘error’ and code = 400 - test_valid_registration() validation successful registration with all parameter properties that are met when performing the post
/v1/register
method with the response json ‘success’ and status = 200
Now run the test
$ phpunit tests/UserRegistrationTest.php
Yes we get an Error message with 5 Test not passed.
Next, lets make one more test for the JWT Auth Login, you may ask
why not put it together with the previous test? The answer is because
The less code we have, the easier it will be to understand, when a block of code is too long, will make it difficult for us to understand the logic flow of a programming language.
Another answer is because in a series of unit tests it is better to make it according to the flow of the case even though there are various features in it.
Create another unit test file with name (tests/UserAuthTest.php
)
$ php artisan make:test UserAuthTest
- test_validate_wrong_user_or_password() validate user error or password when logging in via the post
/v1/login
method with the response json ‘error’ and status code = 400 - test_unauthorized_access_auth_middleware() validate the unauthorized route when accessing the route via the get
/v1/profile
method with response json ‘error’ and status code = 401 - test_valid_login () validation of successful login via post
/v1/login
method with response json ‘success’ and status code = 200 - test_authorized_access_auth_middleware () validate the authorized route when accessing the route via the get
/v1/profile
method with response json ‘success’ and status code = 200
Now run the test
$ phpunit tests/UserAuthTest.php
we get an Error message with 4 Test not passed.
Let’s start developing these features. Open it and please modify the file (bootstrap/app.php
). We will enable config, middleware, facade, elequent and provider dependencies according to our requirements.
...// Register config
$app->configure('app');
$app->configure('auth');
$app->configure('jwt');...// Enable Facades
$app->withFacades();
// Register Eloquent
$app->withEloquent();...// Register Cors Middleware (optional)
$app->middleware([
App\Http\Middleware\CorsMiddleware::class,
]);...// Register auth middleware (shipped with Lumen)
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);...// Register service providers
$app->register(App\Providers\AuthServiceProvider::class);$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);$app->register(Flipbox\LumenGenerator\LumenGeneratorServiceProvider::class);...
create a folder and auth config file (config/auth.php
)
Next we will modify the handle() method on the file
(app/Http/Middleware/Authenticate.php
) for the response ‘Unauthorized 401’
public function handle($request, Closure $next, $guard = null)
{
if ($this->auth->guard($guard)->guest()) {
return response()->json([
'error' => 'Unauthorized',
])->setStatusCode(401);
} return $next($request);
}
Now Environment settings (.env
) match your credentials, if the .env
file doesn’t exist, it can be copied from the file (.env.example
)
APP_NAME="Lumen JWT"
...DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=yourdnname
DB_USERNAME=yourdbusername
DB_PASSWORD=yourdbpassord...
create app secret key with artisan
$ php artisan key:generate
create a JWT secret key with artisan
$ php artisan jwt:secret
In this case, we will use an email for the registration process and require verification of the email email. The email notification verification feature will be made in the second part of the article, for now create a user migration file model first
$ php artisan make:migration create_users_table
open the migration file (database/migrations/…_create_users_table.php
) then comply with the following conditions
...Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('firstname', 50);
$table->string('lastname', 100);
$table->string('email')->unique();
$table->dateTime('email_verified_at')->nullable();
$table->string('password');
$table->timestamp('last_logged_in')->nullable();
$table->timestamps();
});...
modify the User Model file (app/Models/User.php
), add the implementation of JWTSubject class and UUID
...
use Tymon\JWTAuth\Contracts\JWTSubject;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\Provider\Node\RandomNodeProvider;class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
...
protected $fillable = [
'firstname',
'lastname',
'email',
'email_verified_at',
'password',
];
... public $incrementing = false; public static function boot()
{
parent::boot();
static::creating(function ($model) {
$nodeProvider = new RandomNodeProvider();
/* validate duplicate UUID */
do{
$uuid = Uuid::uuid1($nodeProvider->getNode());
$uuid_exist = self::where('id', $uuid)->exists();
} while ($uuid_exist); $model->id = $uuid;
});
} public function getJWTIdentifier()
{
return $this->getKey();
} public function getJWTCustomClaims()
{
return [];
}}
Create an Auth Controller with artisan
$ php artisan make:controller AuthController
Please modify file (app/Http/Controllers/AuthController.php
) with instructions according to the Jwt Auth documentation and update some changes that we will make by adding several methods such as register(), login(), logout(), refresh(), profile() and validation().
Create a user route in the v1
folder (routes/v1/user.php
) where this routes file have a collection of routes related to the previous User and Auth services.
run artisan migrate and make sure the migration is successful
$ php artisan migrate:fresh --seed
It’s time to run the two unit tests that we created earlier again
$ phpunit tests/UserRegistrationTest.php
make sure all test pass successfully
continue with running the next test
$ phpunit tests/UserAuthTest.php
make sure all test pass successfully
We have finished creating the user registration and auth features
Setup Movie Service
As usual we will make the unit test first. Create a unit test file with the name (tests/MovieCRUDTest.php
). Run the artisan command
$ php artisan make:test MovieCRUDTest
- test_show_all() get all paginated movie list using
get/v1/movie/all?page=1&per_page=15
with json response ‘success’ and status code = 200 - test_get_movie_by_id() get movie details using Movie ID parameter and
get/v1/movie/{id}
method with json response ‘success’ and status code = 200 - test_add_viewed_without_auth_should_failed() validation adding movie viewed will fail if unauthorized, using the method
put/v1/movie/{id}/viewed
with response json ‘error’ and status code = 401 - test_add_viewed_with_auth_should_success() validation adding movie viewed will fail if unauthorized, using the method
put/v1/movie/{id}/viewed
with the response son ‘success’ and status code = 200 - test_create_movie() create new movie data using method
post/v1/movie/create
with the response json ‘success’ and staus code = 200 - test_update_movie(): update movie data using method
patch/v1/movie/{id}/update
with response json ‘success’ and status code = 200 - test_delete_movie() delete movie data using method
delete/v1/movie/{id}/delete
with the response json ‘success’ and the status code = 200
run the test
$ phpunit tests/MovieCRUDTest.php
we get an Error message with 7 Test not passed.
Well, the next step we will make an example of a Model Movie along with the migration files, factory, seeder and controller.
$ php artisan make:model Movie -a
Open the migration file (database/migrations/…_create_movies_table.php
) and comply with the following conditions
...Schema::create('movies', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('title', 100);
$table->text('description');
$table->string('embed_url')->unique();
$table->integer('viewed')->default(0);
$table->json('genres')->nullable();
$table->timestamps();
$table->softDeletes();
});...
Modify the Model Movie file (app/Models/Movie.php
)
...
use Ramsey\Uuid\Nonstandard\Uuid;
use Ramsey\Uuid\Provider\Node\RandomNodeProvider;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;class Movie extends Model
{ use HasFactory, SoftDeletes;
protected $fillable = [
'title',
'description',
'embed_url',
'genres',
'viewed',
]; public $incrementing = false;public static function boot()
{
parent::boot();
static::creating(function ($model) {
$nodeProvider = new RandomNodeProvider();
/* validate duplicate UUID */
do{
$uuid = Uuid::uuid1($nodeProvider->getNode());
$uuid_exist = self::where('id', $uuid)->exists();
} while ($uuid_exist); $model->id = $uuid;
}); }}
Modify the Factory Movie file (database/factories/MovieFactory.php
)
... protected $model = Movie::class; public function definition(): array
{
$genres = ['action','adventure','comedy','horror'];
return [
'id' => $this->faker->uuid,
'title' => $this->faker->text(50),
'description' => $this->faker->text(150),
'embed_url' => $this->faker->url,
'genres' => json_encode( array_slice($genres, rand(0, sizeof($genres) - 1)) ),
'viewed' => rand(),
];
}...
Modify the Seeder Movie file (database/seeders/MovieSeeder.php
) by default we will create 100 row seeders
...
use App\Models\Movie;
...public function run()
{
Movie::factory()->count(100)->create();
}...
modify the default Seeder file (database/seeders/DatabaseSeeder.php
) to call the MovieSeeder above.
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
...public function run()
{
/* create default user */
User::create([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => 'john.doe@example.com',
'password' => Hash::make('secret'),
]);
/* create fake movie data */
$this->call(MovieSeeder::class);
}...
Modify Movie Controller file (app/Http/Controllers/MovieController.php
)
Create a movie route in the v1
folder (routes/v1/movie.php
) where this route file holds a collection of routes related to the Movie service above.
run artisan migration
$ php artisan migrate:fresh --seed
It’s time to run the unit test that we created earlier again
$ phpunit tests/MovieCRUDTest.php
make sure all test pass successfully
We have succeeded in making a service movie and it functions according to our expectations.
Now we have to try it, usually we use API Client tools such as POSTMAN, INSOMNIA or the like, but we will use SWAGGER which is commonly used for API documentation and testing.
Setup Swagger
Swagger is very simple and easy to use for API design, testing and documentation. Uses the human-readable data-serialization text languages YAML and JSON. We will use Swagger version 3.x and YAML to test and design our API documentation.
We can use the sawagger editor at the https://editor.swagger.io/ url to design our API documentation. You can access complete documentation about using Swagger on the url https://swagger.io/docs. At the root of our application folder, create a new swagger file with the name (swagger.yaml
) then copy the following code.
Please change the key servers → domain url according to your needs
...servers:
- url: '{protocol}://localhost:8000/{basePath}'
description: Development Server
...
- url: '{protocol}://yourstagingdomain.com/{basePath}'
description: Staging Server
...
- url: '{protocol}://yourproductiondomain.com/{basePath}'
description: Production Server...
Save the file and copy the contents of the file (swagger.yaml
) above then paste it into the online swagger editor https://editor.swagger.io and this is how it looks
Now we can try our API of the services or features that we created previously, namely User, Common and Movie. For example, we will try a service endpoint that does not require authorization.
- Make sure enviroment is a development server and basePath is v1
- Click API
/ping
on the common tag group then click ‘Try it out’
- Click ‘Execute’
- wait until the request process is complete and the response is successfully
In the Responses panel we can see the results of our requests, there is information Server Rsponse Code = 200 and Response body = pong.
Let’s continue with accessing the API endpoint that requires Authorization. The first step is to get a JWT token by logging in first
- Click the API
/login
to the user tag group and click the ‘Try out’ button. In development mode we have created an example user and it was created during the migration and seeders were run in the previous step, namely the user with email john.doe@example.com and password secret. Use this data to log in
- Click the ‘Execute’ button and wait for the response to be received
- If successful, the display will be as shown above, copy the access_token, then click the ‘Authorize’ button at the top of the swagger page.
- Then a popup page will appear to enter the JWT token that was copied earlier, please paste the token in the value field.
- Click the ‘Authorize’ button after saving and then click ‘Close’.
Now we access the API endpoint that requires Authorization, for example/profile
in the user tag group. - Click ‘Try out’ and click the ‘Execute’ button then wait for the response from the server.
If we get a response with code 200 then we can successfully access the user profile service with a JWT token attached to the ‘Authorization’ header.
Now we have finished, you can access this repo at the following link https://github.com/farindra/lumen-jwtauth.git make sure you checkout the part-1
branch according to this stage.
Conclusion
- We have created a backend application using JWT as Authorization method with Lumen 8.
- Our application has a Core library which has the most common functions required for application.
- We have used the Unit Test at the development stage.
- Our application is friendly exception, logging and response.
- Our application has a login service by email, user and movie.
- Our application already has a good API design and documentation
What is next ?
- Make email verification to validate email during registration
- Setup Deployment so that the application can be deployed automatically (CI/CD)
- Docker setup for scalability and availability on production servers
- Application Monitoring Setup