DEV Community

Grant
Grant

Posted on

Implementing Laravel's built-in token authentication

GitHub logo 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

Now we'll run the migrations.

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

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
    ],
],
Enter fullscreen mode Exit fullscreen mode

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
    ],
],
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

We can try it out and generate a token:

➜ php artisan make:token
YHEt7CNf1SBmQs6JbTPf7qMK8FgnynI5SiPmyJELrbAO61heKy0eKuiXrxBJ
Enter fullscreen mode Exit fullscreen mode

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!';
});
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
fxgourves profile image
François-Xavier GOURVES

Great ! Exactly what I needed. Thank you for your experience !
I also learnt about creating commands ! 👌

Collapse
 
zourite profile image
Sonia SAUGRIN

Thanks guy

Collapse
 
featherbits profile image
Elvijs Teikmanis • Edited

Also, you can use Authorization header with Bearer auth scheme instead of api_token query string parameter. Take a look at github.com/laravel/framework/blob/...

Collapse
 
luisya22 profile image
luisya22

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

Collapse
 
grantholle profile image
Grant

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:

public function getAuthIdentifierName()
{
    // Change this to be your id column of your database
    return 'id';
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sebasydoc profile image
Sebastian Rovira

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 ?

Collapse
 
sebasydoc profile image
Sebastian Rovira

It was my bad !! I just had to execute this command:

php artisan config:clear