{%title%}
{%description%}
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.
A LAMP stack on your development machine. I am using MAMP currently to get most of all the features I need to do dev work on my mac, but you are free to use any of the other setups if you got it running already.
A computer to work on… that's a given.
Something like SequelPro or phpMyAdmin for working with your database.
A fresh install of Laravel, can be 5.8.x (or an old project as this doesn’t touch mush old code base.
A code editor, currently mine is VSCode. Sublime is also the other go to.
⏲ and ☕️
An API client tester app:
- Google ARC
- Postman
- or just cURL... :)
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:
And the resulting files from a git status
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=mysql2DB_HOST=127.0.0.13DB_PORT=3306 #or 8889 if using MAMP !4DB_DATABASE=token_api_app5DB_USERNAME=root6DB_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:
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 statements2 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 anapi_token
column on theusers
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 Command10{11 /**12 * The name and signature of the console command.13 *14 * @var string15 */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 string26 */27 protected $description = 'Makes a new API Token';28 29 /**30 * Execute the console command.31 *32 * @return mixed33 */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:
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:
Then you can go ahead and run your command to make a new token:
Then in the database you will see something similar to this:
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\Token11 */12 public $token;13 14 /**15 * The data to persist to mem.16 *17 * @var array18 */19 public $toLog = [];20 21 /**22 * The IP address of the client23 * @var string $ip24 */25 public $ip;26 27 /**28 * Create a new event instance.29 *30 * @param string $ip31 * @param \App\Models\Token $token32 * @param array $toLog33 * @return void34 */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 ShouldQueue11{12 /**13 * Handle the event.14 *15 * @param TokenAuthenticated $event16 * @return void17 */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 ShouldQueue11{12 /**13 * Handle the event.14 *15 * @param TokenFailed $event16 * @return void17 */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:
## 🏁 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:
Well, now add your token that you got earlier under the following section:
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:
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.
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.