Custom Token Authentication for Laravel cover image

Custom Token Authentication for Laravel

Reece May • December 21, 2019 • 14 min read

laravel

Custom Token Authentication for Laravel

🤔 What will be trying to achieve with this?

The idea is to be able to provide access tokens for users. Integrate with the default Laravel Auth system for API requests. And also log the requests that come through, whether successful or not.

Things you will need to get this running.

If you have your dev machine setup already with things, please skip to Step 3


Step 1: Setup your Stack.

I would suggest that you look at the respective install directions for the choice you choose as I don’t think it is necessary to re-write that and make a mess in the transfer of information.

I would suggest that you get your PHP instance up and running as well as mysql before installing things like composer and Laravel. This way you going to have the least headaches.

test the php by running php --version from your terminal.

For installing Laravel and getting it running please follow the official docs. Yes I know it points to 5.8, it has the stuff for setting up a local dev environment. So please use the latest requirements by laravel 6.x or 5.8 depending on your choice.

Step 2: Install a new Laravel app and DB.

Or. If you already have a project, you can go ahead and open that up and move to Begin and collect 20000.

If you have the Laravel installer on your machine, you can run:


laravel new token-api-app

If you are running composer only:

composer create-project --prefer-dist laravel/laravel token-api-app

Once you have got that running, open up the project in your code editor.

You also will want to create your database at this point.

You can use the default laravel .env settings or keep your current DB name.

CREATE DATABASE token_api_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Step 3: Creating your base files for the project.

Because you will be saving tokens that are generated into the database and also their usage stats, we are going to start by making the migration files and the models.

Open up your terminal if you are using an external one. Or you can open up the integrated terminal in VSCode with CMD+J.

3.1 Create your models and migrations for the app

You will now make the the model classes and the migrations using the Laravel artisan make:model command. Using this you will be able to create your two main models that will be used in this project, Token and TokenStatistic.

We are using the --migration option on the command to create the migrations for the database at the same time. (you can also use the short version -m)

php artisan make:model Token --migration
php artisan make:model TokenStatistic --migration

What would be the terminal output results you are looking for: Terminal output

And the resulting files from a git status Created Files 3.2 Modify the created model classes.

You will find your two new model classes under the app directory.

Once you have those created you will replace the Model code with the following files.

Model for the Token class:

The Token model has relationships with the users table and also the token statistics table.

Model code for the TokenStatistic class:

3.3 Editing the migration files:

Our next step is to change the automatically filled in schema. Our schema is going to be pretty basic to get the system working, but obviously have enough useable data.

For your tokens table, delete what is inside the up function, then put the following inside it:

        Schema::create('tokens', function (Blueprint $table) {
            $table->bigIncrements('id');

            $table->unsignedBigInteger('user_id');

            $table->foreign('user_id')
                ->references('id')
                ->on('users');

            $table->string('name');
            $table->string('description')->default('API token');

            $table->string('api_token')->unique()->index();
            $table->integer('limit');

            $table->softDeletes();
            $table->timestamps();
        });

Next, we will change the contents of the token_statistics table, again you will delete the contents of the up function inside the migration file and replace it with the following:

        Schema::create('token_statistics', function (Blueprint $table) {
            $table->unsignedBigInteger('date')->index();

            $table->unsignedBigInteger('token_id')->nullable();
            $table->string('ip_address');
            $table->boolean('success')->default(false);
            $table->json('request');
        });

Step 4: Migrating tables. 💽

Great, you have made it this far 😅. But just before you hit continue; please ensure that inside your .env file you have your database setup correctly.

If you used the code to create your database from this tutorial, please ensure the env file is name correctly:

e.g.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306 #or 8889 if using MAMP !
DB_DATABASE=token_api_app
DB_USERNAME=root
DB_PASSWORD=root

Now we are going to run php artisan migrate in our terminals to create the new tables we have just defined.

This will run the migrations for the default migrations that are provided with Laravel, we want this as it has the users table and also stuff for failed jobs from the queue. Once the migrations are completed we can now move onto the next section.

Should look something like below: migrating tables

note, i have an alias for php artisan -> artisan

Step 5: Lets make our extensions.

We are now going to create our class that is going to extend the the Laravel Illuminate\Auth\EloquentUserProvider. This is the same class that is used as the user provider for the other Auth mechanisms in the Laravel framework.

Now, you are going to create a new file under the following directory:

/app/Extensions/TokenUserProvider.php

Open that new file and place the following code in it

<?php

namespace App\Extensions;

use App\Events\Auth\TokenFailed;
use App\Events\Auth\TokenAuthenticated;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;

class TokenUserProvider extends EloquentUserProvider
{
    use LogsToken;

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (
            empty($credentials) || (count($credentials) === 1 &&
                array_key_exists('password', $credentials))
        ) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();

        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value);
            }
        }
        $token = $query->with('user')->first();

        if (!is_null($token)) {
            $this->logToken($token, request());
        } else {
            $this->logFailedToken($token, request());
        }

        return $token->user ?? null;
    }

    /**
     * Gets the structure for the log of the token
     *
     * @param \App\Models\Token $token
     * @param \Illuminate\Http\Request $request
     * @return void
     */
    protected function logToken($token, $request): void
    {
        event(new TokenAuthenticated($request->ip(), $token, [
            'parameters' => $this->cleanData($request->toArray()),
            'headers' => [
                'user-agent' => $request->userAgent(),
            ],
        ]));
    }

    /**
     * Logs the data for a failed query.
     *
     * @param \App\Models\Token|null $token
     * @param \Illuminate\Http\Request $request
     * @return void
     */
    protected function logFailedToken($token, $request): void
    {
        event(new TokenFailed($request->ip(), $token, [
            'parameters' => $this->cleanData($request->toArray()),
            'headers' => collect($request->headers)->toArray(),
        ]));
    }

    /**
     * Filter out the data to get only the desired values.
     *
     * @param array $parameters
     * @param array $skip
     * @return array
     */
    protected function cleanData($parameters, $skip = ['api_token']): array
    {
        return array_filter($parameters, function ($key) use ($skip) {
            if (array_search($key, $skip) !== false) {
                return false;
            }
            return true;
        }, ARRAY_FILTER_USE_KEY);
    }
}

Lets have a look at the file that we just created. Firstly, we are extending a default Laravel class that handles the normal user resolution when we are using the api auth or any of the other methods, e.g. 'remember_me' tokens.

We are only going to be overriding one of the functions from the parent class in this tutorial as we will do some more with it in future ones.

Our first function then is retrieveByCredentials(array $credentials), there is nothing special about this except two things, we added logging for when the token is successful or fails.

Next we then return the user relationship from the token, and not the model/token as the normal user provider does.

The next two functions handle the login of the data, this can be customized to what you would like to be in there. Especially if you made modifications to the migration file for the tables structure.

The last function is just a simple one that cleans up any data that is in the request based on the keys. This is useful if you want to strip out the api_token that was sent and also to be able to remove any files that are uploaded as they cannot be serialized when the events are dispatched for logging.


Step 6: Registering the services.

We are going to make a custom service provider for this feature as it gives a nice way for you to make adjustments and extend it further, knowing that it is specifically this Service Provider that handles things.

Run the following command from the terminal:

php artisan make:provider TokenAuthProvider

This will now give you your Service Provider stub default.

We will want to import the following classes:

// ... other use statements

use App\Extensions\TokenUserProvider;
use Illuminate\Support\Facades\Auth;

// ...

Now that we have those, we can extend our Auth providers. Replace the boot method with the following piece of code:

   /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Auth::provider('token-driver', function ($app, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\UserProvider...
            return new TokenUserProvider($app['hash'], $config['model']);
        });
    }

Now this uses the Auth provider method to extend the list of provides that Laravel looks for when you define a provider in our config/auth.php file (more on this just now).

We are passing all the parameters from the closure directly to our new class we created in step 5.

Now, having this here is all very good and well, if Laravel's IoC new about it. As it currently is, none of this will do anything until we setup the config files.

First file we are going to edit is the config/app.php file, you will be adding App\Providers\TokenAuthProvider::class under the providers key:

         // App\Providers\BroadcastServiceProvider::class,
         App\Providers\EventServiceProvider::class,
         App\Providers\RouteServiceProvider::class,
+        App\Providers\TokenAuthProvider::class,

     ],

Next, you are going to edit the config/auth.php file.

Under the section guards change it to look like this:

        'api' => [
            'driver' => 'token',
            'provider' => 'tokens',
            'hash' => true,
        ],

        'token' => [
            'driver' => 'token',
            'provider' => 'tokens',
            'hash' => true,
        ]

Explanation: Setting the provider for both of the guards means that Laravel will look at the providers list for one called tokens, that is where we define the driver that we have created as well as what the model is that we are looking at for the api_token column.

Then, add the following under the providers:

        'tokens' => [
            'driver' => 'token-driver',
            'model' => \App\Token::class,
            // 'input_key' => '',
            // 'storage_key' => '',
        ],

The 'tokens.driver' value should match the name that you give when extending the Auth facade in the service provider.

This would complete the configuration needed to let Laravel know what is cutting with the modified auth system.

IMPORTANT Word of caution here with changing the api guard to use the tokens provider, you will have to use the proper tokens generated on the tokens table, and not the normal way that Laravel looks for an api_token column on the users table.


Step 7: Nearly there, lets make some useful additions.

We now really need a way to create tokens. Well, for this tutorial, we are going to use a console command to create the tokens we need. In the follow up articles, we are going to build a UI for managing the tokens and creating them.

To create our command we will run php artisan make:command GenerateApiToken.

In the created command under app/Console/Commands replace everything with the following into the new file:

<?php

namespace App\Console\Commands;

use App\Token;
use Illuminate\Console\Command;
use Illuminate\Support\Str;

class GenerateApiToken extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:token  {name : name the token}
                                        {user : the user id of token owner}
                                        {description? : describe the token}
                                        {l? : the apis limit / min}
                            ';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Makes a new API Token';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->comment('Creating a new Api Token');
        $token = (string)Str::uuid();

        Token::create([
            'user_id'       => $this->argument('user'),
            'name'          => $this->argument('name'),
            'description'   => $this->argument('description') ?? '',
            'api_token'     => hash('sha256', $token),
            'limit'         => $this->argument('l') ?? 60,
        ]);

        $this->info('The Token has been made');
        $this->line('Token is: '.$token);
        $this->error('This is the only time you will see this token, so keep it');
    }
}

Now this command is very simple and would require you to know the users id that the token must link to, but for our purpose now it is perfectly fine.

When we use this command we will only need to provide the user id and a name. If we want we can add a description and a rate limit (this is for future updates)

Now that we have a command to run in the console as the follows:

Create new API token from console

So, we will need to make sure we have a user to create our token for, so fire up php artisan tinker

Inside there we are going to run the following code for a factory factory(\App\User::class)->create()

This will have the following type of result: create new user

Then we can go ahead and run our command to make a new token: Create token

Result from the `make:token` command

Then in the database you will see something similar to this: Token in the database

The token and its hash in the db

Step 9: create the events and listeners.

When logging your token success or failure stats and the headers for each request, we use events and the Laravel queue system to reduce the amount of things done in a request.

If you were to make any requests before creating your event listeners and events you will get a 500 error as it can't find any of the events or listeners.

First file you will want to create is the base class the events will extend as it gives a nice way to have separate event names, but a base class constructor.

Create a file under app/Events/Auth called TokenEvent.php

You will probably have to create that directory as it might not exist.

Inside that file you will place the following code that gets a token, request and ip as its constructor arguments.

<?php

namespace App\Events\Auth;

class TokenEvent 
{
    /**
     * The authenticated token.
     *
     * @var \App\Models\Token
     */
    public $token;

    /**
     * The data to persist to mem.
     *
     * @var array
     */
    public $toLog = [];

    /**
     * The IP address of the client
     * @var string $ip
     */
    public $ip;

    /**
     * Create a new event instance.
     *
     * @param  string  $ip
     * @param  \App\Models\Token $token
     * @param  array $toLog
     * @return void
     */
    public function __construct($ip, $token, $toLog)
    {
        $this->ip    = $ip;
        $this->token = $token;
        $this->toLog = $toLog;
    }
}

In your EventServiceProvider add the following to the listeners array:


\App\Events\Auth\TokenAuthenticated::class => [ \App\Listeners\Auth\AuthenticatedTokenLog::class, ], \App\Events\Auth\TokenFailed::class => [ \App\Listeners\Auth\FailedTokenLog::class, ],

then run php artisan event:generate to create the files automatically.

In each of these files, you will be basically replacing everything.

In the file app/Events/Auth/TokenAuthenticated.php replace everything with:

<?php
// app/Events/Auth/TokenAuthenticated.php

namespace App\Events\Auth;

use Illuminate\Queue\SerializesModels;

class TokenAuthenticated extends TokenEvent
{
    use SerializesModels;

}

The same again for the event file app/Events/Auth/TokenFailed.php, replace everything with:

<?php
// app/Events/Auth/TokenFailed.php

namespace App\Events\Auth;

use Illuminate\Queue\SerializesModels;

class TokenFailed extends TokenEvent
{
    use SerializesModels;

}

For our event listeners now, change the file app/Listeners/Auth/AuthenticatedTokenLog.php

<?php
// app/Listeners/Auth/AuthenticatedTokenLog.php

namespace App\Listeners\Auth;

use App\Events\Auth\TokenAuthenticated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class AuthenticatedTokenLog implements ShouldQueue
{
    /**
     * Handle the event.
     *
     * @param  TokenAuthenticated  $event
     * @return void
     */
    public function handle(TokenAuthenticated $event)
    {
        $event->token->tokenStatistic()->create([
            'date'          => time(),
            'success'       => true,
            'ip_address'    => $event->ip,
            'request'       => $event->toLog,
        ]);
    }
}

Then for our file app/Listeners/Auth/FailedTokenLog.php:

<?php
// app/Listeners/Auth/FailedTokenLog.php

namespace App\Listeners\Auth;

use App\Events\Auth\TokenFailed;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class FailedTokenLog implements ShouldQueue
{
    /**
     * Handle the event.
     *
     * @param  TokenFailed  $event
     * @return void
     */
    public function handle(TokenFailed $event)
    {
        $event->token->tokenStatistic()->create([
            'date'          => time(),
            'success'       => false,
            'ip_address'    => $event->ip,
            'request'       => $event->toLog,
        ]);
    }
}

Both of the event listeners are very simple in that they just take the token that his based to the event and log it against a TokenStatistic::class relation.

For example then a use would be as follows:

event(new TokenAuthenticated($request->ip(), $token, [
    'parameters' => $this->cleanData($request->toArray()),
    'headers' => [
        'user-agent' => $request->userAgent(),
    ],
]));

A successful request would have the following in the table: token log

# 🏁 END: The really exiting part where we see things happen ‼️

To get our local dev server running, just type in to the terminal the following php artisan serve. This will get the php test server running.

Now Laravel comes with default api url route /api/user that returns your currently authenticated user.

Open up your API testing app of choice and type the following into the url input:

http://localhost:8000/api/user

Well, that is going to be quite depressing immediately as you will get a 500 error, similar to this: Error from request

Well, now add your token that you got earlier under the following section: adding the token

Now, press, don't hit the send button.

If all is well in the world of computers you should have a JSON response in the response section of your API tester similar to the below image: response success

We can also do the following with a cURL command from the terminal

Running curl "http://localhost:8000/api/user" You should see a whole load of info come back that means nothing.

Now run either of the following bits of code:

curl -X GET \
  http://localhost:8000/api/user \
  -H 'Authorization: Bearer YOUR_TOKEN_HERE'

or

curl "http://localhost:8000/api/user?api_token=YOUR_TOKEN_HERE"

You will then see a JSON response similar to the Postman/ARC one:

{
    "id": 1,
    "name": "Valerie Huels DDS",
    "email": "herman.orion@example.com",
    "email_verified_at": "2019-12-20 23:07:17",
    "created_at": "2019-12-20 23:07:17",
    "updated_at": "2019-12-20 23:07:17"
}

Conclusion

So congratulations, you have made it this far and put up with my strange style of explaining things or maybe you are like me and skipped all the way to the end in search of a package or something.

Well you can have a demo application that has all of this wonderfully functioning code running in it to pull a TL;DR on.

ReeceM/laravel-token-demo

Thank you very much for putting up with my first post here and tutorial on Laravel. Please leave any suggestions in the comments or PRs on the repo if you feel so inclined.

There will be some follow up articles for this, so please keep an eye out for them.

Reece - o_0x