grantholle / laravel-token-auth-example
This is a basic implementation of Laravel's default "token" auth driver.
One of the great things about Laravel is its mission to provide developers with the tools they need out of the box, as easily as possible.
More often than not when developing an application you're going to need some mechanism of authentication. Up until recently, Laravel shipped with a complete authentication toolbox: controllers, routes and views. Recently Laravel migrated a lot of its backend authentication functionality into Laravel Fortify and provided a frontend simple implementation using Breeze. There's also a more opinionated auth setup using JetStream which combines Fortify and other currently-popular frontend tools Livewire and (my personal favorite) Inertiajs.
But what about API's? Laravel provides two solutions, Sanctum and Passport. Both of these are quite fully featured; they come with all the tools for users to generate tokens for themselves to interact with your application.
But recently I needed to implement very simple API authentication for interacting with an application from a different application. Think "machine to machine" interaction. Passport ships with an entire OAuth implementation, which includes client credentials grant tokens. The gist is that you provide a token to authenticate without any of the traditional OAuth flow.
While Passport is a great tool, it includes way more than I needed. Lucky for you and me, Laravel already ships with a token-based authentication mechanism. Unfortunately it's actually quite undocumented.
Begin!
I'm starting with a clean project using PHP 8 (version isn't important, just being thorough) for this tutorial, but I added it to my existing project using the same logic. What's really great is that there are no packages we need to install. Laravel ships with all the tools we need.
The important thing to note here is that for this example, I needed to perform some actions at the admin level, not anything based on a particular user account. I actually don't care who is doing this API action (i.e. what the value of $request->user()
is), just that I needed to do something remotely that is triggered by a different system, sort of like a microservice.
Create the sample project
Create the new project.
laravel new token-auth-example
You'll need to set up your own database and know how to configure it to get it going. I'm not going to cover that here.
Since we don't care about users right now, we can ignore that entirely.
Create the token table
Next, let's create a new migration for a table called api_clients
.
php artisan make:migration --create=api_clients create_api_clients_table
I'll modify it to have just a column, api_token
. This is the column that Laravel will use to look up the token:
Schema::create('api_clients', function (Blueprint $table) {
$table->id();
$table->string('api_token')->unique();
});
Now we'll run the migrations.
php artisan migrate
Configure the api guard and provider
We need to make the necessary changes to config/auth.php
to configure our token authentication. This was confusing for me for a while to understand what the different keys mean, but I'll try myself to explain it.
The guards are what protect parts of your application. Laravel allows you to create separate guards so theoretically you could have different user models that use different parts of your application. I've done this and it can get a little messy, but it's nice that Laravel is flexible enough to allow this. By default it has two generic guards, "web" and "api". The web guard is for authenticating into your web app via the browser and the api guard is for, you guessed it, api access. There are separate guards because a browser and api are two different mediums of interacting with your application.
The default configuration is pretty close to what we need.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'api_clients', // was users
'hash' => true, // was false
],
],
We've modified the provider
key to be api_clients
(more on that next) and then set hash
to be true. The hash
option means that we're going to store the token hashed (i.e. not in plain text)
Next we'll need to configure its provider. The provider "provides" who the user is that is being authenticated. Your users (in our case clients) are stored in the database, and we need to tell Laravel how to retrieve the user to verify they're valid.
The default users
provider is that it's your App\Models\User
Eloquent model. For our api client, we could use Eloquent too, but again, we don't care who it is. We don't need Eloquent's bells and whistles. We just need a table, which we've already created.
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'api_clients' => [ // <- Add the api_clients that was configured in our `api` guard
'driver' => 'database', // We don't need eloquent
'table' => 'api_clients', // Change to be our table name, which happens to be the same as our provider name
],
],
Believe it or not, that's all we need for Laravel to automatically do token authentication in our api routes. But... how do we create clients?
Creating API clients
For our simple use-case, we don't need routes to manage tokens and all of that jazz. We need one token to exist at a time. In times like this, I reach for an Artisan command.
php artisan make:command GenerateAuthToken
This will give us all the functionality we need: remove old tokens and create a new one, providing the new token to me to use. In app/Console/Commands/GenerateAuthToken.php
we can add this in a few lines.
Change the signature and description to something more meaningful:
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:token';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generates a new api client auth token to consume our api';
Remove the code for the constructor because we don't need it. In handle()
we need to create a token, remove old clients and create the new client with the new token.
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
public function handle()
{
// Generate a new long token
// Be sure to import Illuminate\Support\Str at the top
$token = Str::random(60);
// Delete existing tokens, maybe we lost the token
// so we don't want existing ones floating around somewhere
DB::table('api_clients')->whereNotnull('api_token')->delete();
// Create a new entry with the hashed token value
// so we don't store the token in plain text
DB::table('api_clients')->insert([
'api_token' => hash('sha256', $token),
]);
// Spit out the token so we can use it
$this->info($token);
return 0;
}
We can try it out and generate a token:
➜ php artisan make:token
YHEt7CNf1SBmQs6JbTPf7qMK8FgnynI5SiPmyJELrbAO61heKy0eKuiXrxBJ
If we check out the database, the value for api_token
is hashed as 277b302457eeccc1607b024b16f6c50bfaf37d7db028f9bf1daf4add58282a57
. When we make a request, Laravel will hash our token to match it against what's in the database, rather than comparing the raw token. This also means that you better put that token somewhere safe, as that's the only time you'll be able to see it.
Implement a route
Head on over to routes/api.php
and change the route to something else:
Route::middleware('auth:api')->get('/test', function (Request $request) {
return 'Authenticated!';
});
For this simple example, we are using a Closure. You would probably use a real controller, such as a single action controller.
There are a few ways we can use our token to access this route. The main ways are either passing a variable in the query string of the request or using an Authentication
header Bearer
token. I'll just use the browser with a query string. I'm going to use Laravel's php artisan serve
to test it out quickly.
I can now go to http://localhost:8000/api/test?api_token=YHEt7CNf1SBmQs6JbTPf7qMK8FgnynI5SiPmyJELrbAO61heKy0eKuiXrxBJ
to see if it worked.
I do in fact see "Authenticated!". Awesome! I'm going to modify the token, or even remove it entirely, to see what happens.
You're going to see a "Route [login] not defined" error. This just means that we weren't authenticated and it tried to redirect us to a login page, which doesn't exist.
Conclusion
For my use case of performing an admin task in my application from a different application (server to server/machine-to-machine), this token authentication is just what I needed. The best part was that I didn't need any additional package as Laravel already had the token driver I needed. An Artisan command gave me all the functionality I needed to manage clients and tokens. We're all set!
Top comments (7)
Great ! Exactly what I needed. Thank you for your experience !
I also learnt about creating commands ! 👌
Thanks guy
Also, you can use
Authorization
header withBearer
auth scheme instead ofapi_token
query string parameter. Take a look at github.com/laravel/framework/blob/...Undefined index: id in file \vendor\laravel\framework\src\Illuminate\Auth\GenericUser.php on line 44
When trying to make a request I receive this error
Hey! It sounds like your
User
model doesn't have an id?Whatever your "user" is will need to return your identifying attribute. By default it's id, but you can add this function to your
User
(or whatever) to return the right id column:When I execute the last step, I have an error -> InvalidArgumentException: Auth guard [api] is not defined.
Do you know what could be the problem ?
It was my bad !! I just had to execute this command: