How to Use Multiple Authentication Providers in Lumen

December 03, 2021
Written by
Kenneth Ekandem
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Use Multiple Authentication Providers in Lumen

As we all know, authentication is a very important aspect of building an application because you want to ensure that users can only access routes and information that they're allowed to.

Authentication takes many forms and a common one is tokenization which we will be focusing on in this tutorial.

Tokenization replaces a sensitive data element, for example, user information such as user id's, names, and emails, with a non-sensitive substitute, known as a token.

But how will issuing tokens be beneficial to multiple authentications? Well, since every user has a unique token, and they can be stored in separate database tables, the token can be queried across these tables to find a match, and based on which return a true, the route can be limited or opened for the user.

In this tutorial, you will learn how to use multiple authentication providers in Lumen to limit access to routes, depending on whether the logged-in user is an admin or not.

So with that out of the way, let us dive in and create a Lumen application with multiple authentication providers.

Prerequisites

  1. Basic knowledge of Laravel and Lumen
  2. Composer installed globally
  3. Postman for testing of APIs
  4. A database, such as SQLite, MariaDB, or PostgreSQL

Install and configure Lumen

To create a new Lumen-based project and switch to it, in your terminal run the code below.

composer create-project --prefer-dist laravel/lumen lumen_multiple_authentication
cd lumen_multiple_authentication

Then, in .env, set APP_KEY to a 32 character, random string, which you can generate at https://onlinerandomtools.com/generate-random-string. Unlike Laravel, Lumen doesn't support the functionality to auto-generate the string.

Create the database migrations

If this is your first time hearing about Lumen, it is a micro-framework built by Laravel, which is backend-centered. Given that, we will need to connect our application to a database so it can run migrations and add data.

To connect to the database, in your preferred editor or IDE, open .env, located in the root directory of the project, and change the database-specific variables to connect to your database.

DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

The next step is to create database migrations. These will create the tables and load data into the tables through seeding. To do that, run the commands below.

php artisan make:migration create_users_table
php artisan make:migration create_admin_table

The commands create two new migration files in the database/migrations folder. Each file's filename starts with the current date and timestamp in the format YYYY_MM_DD_HHMMSS, for example 2021_11_17_120432 and ends with the name given to the make:migration command.

So, allowing for the date and time that you ran the commands, you should see two files named 2021_11_17_120432_create_users_table.php and 2021_11_17_120441_create_admin_table.php.

Update the up function in the migration ending with create_users_table to match the code below.

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password');
    $table->text('api_token')->nullable();
    $table->timestamps();
});

Next, update the up function in the migration ending with create_admin_table to match the code below.

Schema::create('admin', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password');
    $table->text('api_token')->nullable();
    $table->timestamps();
});

After you have updated the files, run the migration by running the command below.

php artisan migrate

Seed the database

So to seed a new user/admin into the tables we just created for them, we will create two database seeders, database/seeders/UsersSeeder.php and database/seeders/AdminsSeeder.php. The seeders will include the information for a new user/admin, which we will be using later in the article, to validate their existence, authenticate them, and limit the access based on their permissions.

If you want to prefer to seed the database using a RESTful API, have a read of this article.

To generate them, run the commands below.

php artisan make:seeder UsersSeeder
php artisan make:seeder AdminsSeeder

After the command has been completed, update the run function in database/seeders/UsersSeeder.php to match the code below.

public function run()
{
    DB::table('users')->insert([
        'name' => 'User 1',
        'email' => 'user1@gmail.com',
        'password' => Hash::make('password')
    ]);
}

Then, update the run function in database/seeders/AdminsSeeder.php to match the code below.

public function run()
{
    DB::table('admin')->insert([
        'name' => 'User 1',
        'email' => 'user1@gmail.com',
        'password' => Hash::make('password')
    ]);
}

Don't forget to include the two use statements below, to the top of both seeders.

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

Once the Seeders are updated, it's time to seed to the database, by running the commands below.

php artisan db:seed --class=UsersSeeder
php artisan db:seed --class=AdminsSeeder

Now that the  database has been seeded, we can then install Laravel Passport into our application to create tokens for authorization.

Install and configure Laravel Passport

To download Laravel Passport, we will not use the conventional documentation for Laravel, as it is buggy, and Laravel Passport is not, currently, fully supported in Lumen. Instead, we will use the dusterio/lumen-passport package. Run the following command in the root of your project directory to install it.

composer require dusterio/lumen-passport

Then, open bootstrap/app.php and update it to match the code below.


<?php

require_once __DIR__.'/../vendor/autoload.php';

(new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
        dirname(__DIR__)
))->bootstrap();

date_default_timezone_set(env('APP_TIMEZONE', 'UTC'));

$app = new \Dusterio\LumenPassport\Lumen7Application(
        dirname(__DIR__)
);

$app->withFacades();
$app->withEloquent();

$app->singleton(
        Illuminate\Contracts\Debug\ExceptionHandler::class,
        App\Exceptions\Handler::class
);

$app->singleton(
        Illuminate\Contracts\Console\Kernel::class,
        App\Console\Kernel::class
);

$app->configure('app');

$app->routeMiddleware([
        'auth' => App\Http\Middleware\Authenticate::class,
]);

$app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);
$app->register(App\Providers\EventServiceProvider::class);
$app->register(Laravel\Passport\PassportServiceProvider::class);
$app->register(Dusterio\LumenPassport\PassportServiceProvider::class);

$app->router->group([
        'namespace' => 'App\Http\Controllers',
], function ($router) {
        require __DIR__.'/../routes/web.php';
});

return $app;

Next, create a new directory named config and in that directory, using your preferred text editor or IDE, create a file named auth.php. In that file add the guard configuration below, which supports Laravel Passport.

<?php 

return [
    'defaults' => [
        'guard' => 'api',
        'passwords' => 'users',
    ],

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

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => \App\Models\User::class
        ]
    ]
];

The above code will be modified, later, to accommodate both user and admin guards for multiple authentication providers.

After that, run the migrations and install the encryption keys for Laravel Passport, by running the commands below.

php artisan migrate
php artisan passport:install

The second command will result in output similar to the example below to your terminal.

Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: hvBtHWUycGLQIDuTOLvm3o9BvNGlO2acVyUhoU8O

Add the following key/value pairs to the end of .env, and replace the <client id> and <client secret> pair with the Client ID and Client secret values respectively, that were output to the terminal.

PASSPORT_PERSONAL_ACCESS_CLIENT_ID="<client id>"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="<client secret>"

Create the controllers

Now Laravel Passport is installed and configured, it's time to add controllers to the application along with the applicable routes.

To create a new controller, copy app/Http/Controllers/ExampleController.php and create a new file UserController.php, and rename the class to UserController. Then, update the body of UserController.php with the code below which:

  • Validates the information passed to it in the requestGenerated a Passport token
  • Updates the api_token for that particular user to include the newly generated token
public function login(Request $request) 
{
    // Validate passed parameters
    $this->validate($request, [
        'email' => 'required',
        'password' => 'required'
    ]);

    // Get the user with the email
    $user = User::where('email', $request['email'])->first();

    // check is user exist
    if (!isset($user)) {
        return response()->json(
            [
                'status' => false,
                'message' => 'User does not exist with this details'
            ]
        );
    }

    // confirm that the password matches
    if (!Hash::check($request['password'], $user['password'])) {
        return response()->json(
            [
                'status' => false,
                'message' => 'Incorrect user credentials'
            ]
        );
    }

    // Generate Token
    $token = $user->createToken('AuthToken')->accessToken;

    // Add Generated token to user column
    User::where('email', $request['email'])->update(array('api_token' => $token));

    return response()->json(
        [
            'status' => true,
            'message' => 'User login successfully',
            'data' => [
                'user' => $user,
                'api_token' => $token
            ]
        ]
    );
}

public function profile() 
{
    $user = Auth::user();

    return response()->json(
        [
            'status' => true,
            'message' => 'User profile',
            'data' => $user
        ]
    );
}

public function all()
{
    $users = User::all();

    return response()->json(
        [
            'status' => true,
            'message' => 'All users',
            'data' => $users
        ]
    );
}

Then, add the use statements in the example below to the top of the class.

 

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

Then we create a new model class for `Admin` in app/Models/Admin.php, and add the code below to it; this model ties it to our existing database table.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Passport\HasApiTokens;

class Admin extends Model
{
    use HasApiTokens;

    public $table = 'admin';
    public $primaryKey = 'id';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'api_token'
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password',
    ];
}

I added HasApiTokens which only gives permission to accepted users with cards or other means of transaction.

Then create a new file named app/Http/Controllers/AdminController.php the same way that app/Http/Controllers/UserController.php was created, and paste the code below as the body of the new controller.

public function login(Request $request) 
{
    // Validate passed parameters
    $this->validate($request, [
        'email' => 'required',
        'password' => 'required'
    ]);

    // Get the admin with the email
    $admin = Admin::where('email', $request['email'])->first();

    // check is user exist
    if (!isset($admin)) {
        return response()->json(
            [
                'status' => false,
                'message' => 'User does not exist with this details'
            ]
        );
    }

    // confirm that the password matches
    if (!Hash::check($request['password'], $admin['password'])) {
        return response()->json(
            [
                'status' => false,
                'message' => 'Incorrect user credentials'
            ]
        );
    }

    // Generate Token
    $token = $admin->createToken('AuthToken')->accessToken;

    // Add Generated token to user column
    Admin::where('email', $request['email'])->update(array('api_token' => $token));

    return response()->json(
        [
            'status' => true,
            'message' => 'User login successfully',
            'data' => [
                'user' => $admin,
                'api_token' => $token
            ]
        ]
    );
}

Then, add the following use statements to the top of the class.

use App\Models\Admin;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

Finally, in app/Models/User.php, add HasApiTokens to the list of traits that the class uses, and add the following use statement to the top of the class.

Use laravel\Passport\HasApiTokens;

Add the Guard configuration

It is important to add authentication guards for both the user and admin routes to point to Passport as the default driver for token authentication. This will come in handy when you want to restrict/allow the user/admin to a particular route.

To do this, go to config/auth.php and modify the existing code to match the example below.

use App\Models\Admin;
use App\Models\User;

return [
    'defaults' => [
        'guard' => 'api',
        'passwords' => 'users',
    ],
    'guards' => [
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'passport',
            'provider' => 'admins',
        ]
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => User::class
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model' => Admin::class
        ]
    ]
];

Routes

After we are done with the controllers, we can then create our routes to link to the controller. Open routes/web.php and add the routes after the existing route definition.

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->group(['prefix' => 'auth'], function ($router) {
    $router->post('login', 'UserController@login');
});
$router->group(['prefix' => 'user', 'middleware' => 'auth'], function ($router) {
    $router->get('profile', 'UserController@profile');
});

$router->group(['prefix' => 'admin'], function ($router) {
    $router->post('login', 'AdminController@login');

    $router->group(['prefix' => 'users', 'middleware' => 'auth'], function ($router) {
        $router->get('', 'UserController@all');
    });
});

Build multiple authentication providers

Next, we need to update the authentication providers to authenticate the user/admin based on the route. To do this, add the following code to the boot method in app/Providers/AuthServiceProvider.php.

$this->app['auth']->viaRequest('passport', function ($request) 
{
    if ($request->header('Authorization')) {
        $key = explode(' ', $request->header('Authorization'))[1];

        if ($request->is('admin/*')) {
            return Admin::where('api_token', $key)->first();
        } else {
            return User::where('api_token', $key)->first();
        }
    }
});

In addition, add the following use statements to the top of the class.

use App\Models\Admin;
use App\Models\User;

This will authenticate routes using a Laravel Passport token, go to the header to the authorization token, and then depending on the requested route, check to confirm if the api_token is valid or not. If the token does not exist in the database and does not match the request database for that route, it returns an HTTP 401 Unauthorized response.

That is it for multiple authentications in the platform, we can now go ahead and test our project to confirm it works just fine.

Test that everything works

Now, it's time to test that the code works. To do that, we will be using Postman, which is an application used for API testing. It is an HTTP client that tests HTTP requests, utilizing a graphical user interface, through which we obtain different types of responses that need to be subsequently validated.

First, start the application by running the following command in the root directory of your project.

php -S localhost:8000 -t public

Login as a user

Next, test that a user can login. To do that:

  • Set the request type to POST
  • Set the request URL to http://localhost:8000/auth/login
  • Under the Body tab, set the form encoding type to "raw" and set the body content to be JSON, in the drop-down list on the far right hand side.
  • Paste the JSON below into the body field. It contains the user login details that we seeded the database with earlier.
  • Send the request by clicking Send
{
    "email": "user1@gmail.com",
    "password": "password"
}

First test using Postman

The request will return a response containing the API token which must be passed in a header with requests to user-permitted routes.

Login as an admin

To login as an admin, use the same settings as for logging in as a user, in the previous example, but set the request URL to http://localhost:8000/admin/login.

View all users

Next, let's test that all users can only be listed by admin users. To do that:

  • Set the request type to GET
  • Set the request URL to http://localhost:8000/admin/users
  • Under the Authorization tab, set the type to "Bearer Token", and use the admin api_token that was generated and stored in the database for the "Token" field's value.

Then, make the request and you should see all of the users in the database listed in the response panel.

Second test using Postman

Conclusion

Thank you for working through this tutorial. In it, we covered how to set up a Lumen application, install and configure Passport and then use it to generate tokens that we can use to authenticate users/admin. Then we went ahead to create multiple authentication and permission for different routes and check if the passed tokens are valid.

If you want to view the full codebase for this article, you can go to the public repository I created, and I hope you found it super useful.

Kenneth Ekandem is a full-stack developer from Nigeria currently in the blockchain space, but interested in learning everything computer science has to offer. He'd love to go to space one day and also own his own vlogging channel to teach the next generation of programmers.