With this tutorial I will skip the “... a recent project I have been working on...” bit followed by an explanation of its details and things, I really think everybody is probably busy with some sort of side project and having a 'today I learned moment'. 🙃
🤔 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.
Disclaimer at this point in time right now: I am not fully sure myself if the methods used are 'secure' as I am not a security specialist, duh, I do know however that it works and is a good lesson in extending the Laravel auth sections.
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:
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:
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:
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.
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 anapi_token
column on theusers
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:
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:
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:
🏁 END: The really exciting 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:
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:
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
A demo for a custom Laravel API token authentication tutorial
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.
I would really appreciate it if you enjoyed this article and feel generous to buying a coffee or something else. (I am trying to create a new application for the php community)
Reece - o_0
Top comments (0)