Custom Token Authentication for Laravel

Reece May • December 21, 2019 • 11 min read

laravel

🤔 What will you 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:

1 
2laravel new token-api-app

If you are running composer only:

1composer 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.

1CREATE 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, you 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.

You 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)

1php artisan make:model Token --migration
1php 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:

Your next step is to change the automatically filled in schema. Your 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:

1Schema::create('tokens', function (Blueprint $table) {
2 $table->bigIncrements('id');
3 
4 $table->unsignedBigInteger('user_id');
5 
6 $table->foreign('user_id')
7 ->references('id')
8 ->on('users');
9 
10 $table->string('name');
11 $table->string('description')->default('API token');
12 
13 $table->string('api_token')->unique()->index();
14 $table->integer('limit');
15 
16 $table->softDeletes();
17 $table->timestamps();
18});

Next, you 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:

1Schema::create('token_statistics', function (Blueprint $table) {
2 $table->unsignedBigInteger('date')->index();
3 
4 $table->unsignedBigInteger('token_id')->nullable();
5 $table->string('ip_address');
6 $table->boolean('success')->default(false);
7 $table->json('request');
8});

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 looks similar to this:

e.g.

1DB_CONNECTION=mysql
2DB_HOST=127.0.0.1
3DB_PORT=3306 #or 8889 if using MAMP !
4DB_DATABASE=token_api_app
5DB_USERNAME=root
6DB_PASSWORD=root

Now you are going to run php artisan migrate in your terminal to create the new tables you have just defined.

This will run the migrations for the default migrations that are provided with Laravel, you want this as it has the users table and also stuff for failed jobs from the queue. Once the migrations are completed you 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 your extensions.

You are now going to create your 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

1<?php
2 
3namespace App\Extensions;
4 
5use App\Events\Auth\TokenFailed;
6use App\Events\Auth\TokenAuthenticated;
7 
8use Illuminate\Auth\EloquentUserProvider;
9use Illuminate\Contracts\Support\Arrayable;
10use Illuminate\Support\Str;
11 
12class TokenUserProvider extends EloquentUserProvider
13{
14 use LogsToken;
15 
16 /**
17 * Retrieve a user by the given credentials.
18 *
19 * @param array $credentials
20 * @return \Illuminate\Contracts\Auth\Authenticatable|null
21 */
22 public function retrieveByCredentials(array $credentials)
23 {
24 if (
25 empty($credentials) || (count($credentials) === 1 &&
26 array_key_exists('password', $credentials))
27 ) {
28 return;
29 }
30 
31 // First you will add each credential element to the query as a where clause.
32 // Then you can execute the query and, if you found a user, return it in a
33 // Eloquent User "model" that will be utilized by the Guard instances.
34 $query = $this->newModelQuery();
35 
36 foreach ($credentials as $key => $value) {
37 if (Str::contains($key, 'password')) {
38 continue;
39 }
40 
41 if (is_array($value) || $value instanceof Arrayable) {
42 $query->whereIn($key, $value);
43 } else {
44 $query->where($key, $value);
45 }
46 }
47 $token = $query->with('user')->first();
48 
49 if (!is_null($token)) {
50 $this->logToken($token, request());
51 } else {
52 $this->logFailedToken($token, request());
53 }
54 
55 return $token->user ?? null;
56 }
57 
58 /**
59 * Gets the structure for the log of the token
60 *
61 * @param \App\Models\Token $token
62 * @param \Illuminate\Http\Request $request
63 * @return void
64 */
65 protected function logToken($token, $request): void
66 {
67 event(new TokenAuthenticated($request->ip(), $token, [
68 'parameters' => $this->cleanData($request->toArray()),
69 'headers' => [
70 'user-agent' => $request->userAgent(),
71 ],
72 ]));
73 }
74 
75 /**
76 * Logs the data for a failed query.
77 *
78 * @param \App\Models\Token|null $token
79 * @param \Illuminate\Http\Request $request
80 * @return void
81 */
82 protected function logFailedToken($token, $request): void
83 {
84 event(new TokenFailed($request->ip(), $token, [
85 'parameters' => $this->cleanData($request->toArray()),
86 'headers' => collect($request->headers)->toArray(),
87 ]));
88 }
89 
90 /**
91 * Filter out the data to get only the desired values.
92 *
93 * @param array $parameters
94 * @param array $skip
95 * @return array
96 */
97 protected function cleanData($parameters, $skip = ['api_token']): array
98 {
99 return array_filter($parameters, function ($key) use ($skip) {
100 if (array_search($key, $skip) !== false) {
101 return false;
102 }
103 return true;
104 }, ARRAY_FILTER_USE_KEY);
105 }
106}

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

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

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

Next you 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.

You 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:

1php artisan make:provider TokenAuthProvider

This will now give you your Service Provider stub default.

You will want to import the following classes:

1// ... other use statements
2 
3use App\Extensions\TokenUserProvider;
4use Illuminate\Support\Facades\Auth;
5 
6// ...

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

1/**
2 * Bootstrap services.
3 *
4 * @return void
5 */
6 public function boot()
7 {
8 Auth::provider('token-driver', function ($app, array $config) {
9 // Return an instance of Illuminate\Contracts\Auth\UserProvider...
10 return new TokenUserProvider($app['hash'], $config['model']);
11 });
12 }

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

You are passing all the parameters from the closure directly to your new class you 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 you setup the config files.

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

1 // App\Providers\BroadcastServiceProvider::class,
2 App\Providers\EventServiceProvider::class,
3 App\Providers\RouteServiceProvider::class,
4+ App\Providers\TokenAuthProvider::class,
5 
6 ],

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

Under the section guards change it to look like this:

1'api' => [
2 'driver' => 'token',
3 'provider' => 'tokens',
4 'hash' => true,
5],
6 
7'token' => [
8 'driver' => 'token',
9 'provider' => 'tokens',
10 'hash' => true,
11]

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 you define the driver that you have created as well as what the model is that you are looking at for the api_token column.

Then, add the following under the providers:

1'tokens' => [
2 'driver' => 'token-driver',
3 'model' => \App\Token::class,
4 // 'input_key' => '',
5 // 'storage_key' => '',
6],

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.

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

To create your command you will run php artisan make:command GenerateApiToken.

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

1<?php
2 
3namespace App\Console\Commands;
4 
5use App\Token;
6use Illuminate\Console\Command;
7use Illuminate\Support\Str;
8 
9class GenerateApiToken extends Command
10{
11 /**
12 * The name and signature of the console command.
13 *
14 * @var string
15 */
16 protected $signature = 'make:token {name : name the token}
17 {user : the user id of token owner}
18 {description? : describe the token}
19 {l? : the apis limit / min}
20 ';
21 
22 /**
23 * The console command description.
24 *
25 * @var string
26 */
27 protected $description = 'Makes a new API Token';
28 
29 /**
30 * Execute the console command.
31 *
32 * @return mixed
33 */
34 public function handle()
35 {
36 $this->comment('Creating a new Api Token');
37 $token = (string)Str::uuid();
38 
39 Token::create([
40 'user_id' => $this->argument('user'),
41 'name' => $this->argument('name'),
42 'description' => $this->argument('description') ?? '',
43 'api_token' => hash('sha256', $token),
44 'limit' => $this->argument('l') ?? 60,
45 ]);
46 
47 $this->info('The Token has been made');
48 $this->line('Token is: '.$token);
49 $this->error('This is the only time you will see this token, so keep it');
50 }
51}

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

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

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

Create new API token from console

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

Inside there you 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 you can go ahead and run your 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, you 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.

1<?php
2 
3namespace App\Events\Auth;
4 
5class TokenEvent
6{
7 /**
8 * The authenticated token.
9 *
10 * @var \App\Models\Token
11 */
12 public $token;
13 
14 /**
15 * The data to persist to mem.
16 *
17 * @var array
18 */
19 public $toLog = [];
20 
21 /**
22 * The IP address of the client
23 * @var string $ip
24 */
25 public $ip;
26 
27 /**
28 * Create a new event instance.
29 *
30 * @param string $ip
31 * @param \App\Models\Token $token
32 * @param array $toLog
33 * @return void
34 */
35 public function __construct($ip, $token, $toLog)
36 {
37 $this->ip = $ip;
38 $this->token = $token;
39 $this->toLog = $toLog;
40 }
41}

In your EventServiceProvider add the following to the listeners array:

1 
2\App\Events\Auth\TokenAuthenticated::class => [
3 \App\Listeners\Auth\AuthenticatedTokenLog::class,
4],
5\App\Events\Auth\TokenFailed::class => [
6 \App\Listeners\Auth\FailedTokenLog::class,
7],

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:

1<?php
2// app/Events/Auth/TokenAuthenticated.php
3 
4namespace App\Events\Auth;
5 
6use Illuminate\Queue\SerializesModels;
7 
8class TokenAuthenticated extends TokenEvent
9{
10 use SerializesModels;
11 
12}

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

1<?php
2// app/Events/Auth/TokenFailed.php
3 
4namespace App\Events\Auth;
5 
6use Illuminate\Queue\SerializesModels;
7 
8class TokenFailed extends TokenEvent
9{
10 use SerializesModels;
11 
12}

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

1<?php
2// app/Listeners/Auth/AuthenticatedTokenLog.php
3 
4namespace App\Listeners\Auth;
5 
6use App\Events\Auth\TokenAuthenticated;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Queue\InteractsWithQueue;
9 
10class AuthenticatedTokenLog implements ShouldQueue
11{
12 /**
13 * Handle the event.
14 *
15 * @param TokenAuthenticated $event
16 * @return void
17 */
18 public function handle(TokenAuthenticated $event)
19 {
20 $event->token->tokenStatistic()->create([
21 'date' => time(),
22 'success' => true,
23 'ip_address' => $event->ip,
24 'request' => $event->toLog,
25 ]);
26 }
27}

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

1<?php
2// app/Listeners/Auth/FailedTokenLog.php
3 
4namespace App\Listeners\Auth;
5 
6use App\Events\Auth\TokenFailed;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Queue\InteractsWithQueue;
9 
10class FailedTokenLog implements ShouldQueue
11{
12 /**
13 * Handle the event.
14 *
15 * @param TokenFailed $event
16 * @return void
17 */
18 public function handle(TokenFailed $event)
19 {
20 $event->token->tokenStatistic()->create([
21 'date' => time(),
22 'success' => false,
23 'ip_address' => $event->ip,
24 'request' => $event->toLog,
25 ]);
26 }
27}

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:

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

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

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

To get your 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:

1http://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

You 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:

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

or

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

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

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

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

Syntax highlighting by Torchlight.