We've recently launched Place Checkup, a web application that allows users to check if a given place follows the WHO recommended safety and prevention measures against COVID-19. Business owners, which are not yet Infraspeak's customers, can register through this website, claim their places and register how and what measures they're actually following. In the end, they get an A, B, or C badge qualifying their places.

Since Infraspeak's core software business is directly related to infrastructure maintenance management, and since we develop with an API-first approach, we already had the core functionalities available to easily use on Place Checkup. Besides that, and to avoid having multiple accounts spread over different projects, we defined that we'd leverage our existent API authentication, and centralize all the accounts there.

Our API uses Passport, an oAuth2 server implementation for Laravel projects, so by sending a user’s credentials to the API service, if they are valid, it returns an encoded JWT with the user information we’d need. To keep things simple, and to avoid having to implement a Machine-to-Machine authentication, we wanted to simply store the generated JWT in session, on Place Checkup, for further usage on subsequent calls to the API.

Laravel has a very good, and extendable, authentication system, referenced throughout the documentation as Guards. Their job is to know how an authentication request gets processed and how to communicate with a Provider, whose responsibility is to know how to get the requested data from whatever persistent layer it handles.

If correctly implemented, a custom Guard would allow us to tap into middlewares, facades, and helper methods commonly used within Laravel to handle authentication, like the Auth facade, or getting the currently authenticated user via $request->user(), or even use the auth middleware to secure access to private routes. That looked exactly what we needed for this project!

But adding a custom Guard and Provider proved to be a little more difficult than what the documentation shows to be. Reading through the sections that describe how Guards and Providers work, I could only see how to register the custom classes to the Container, which interfaces to implement, and which type of classes it needs to return. I couldn't quite get a clear picture of how the flow was, so I realized I'd need to dive deep into the authentication system and try to map it from within.

Understanding Guards

Laravel's documentation describes Guards as the following:

Guards define how users are authenticated for each request.

But, what does that really mean? That was the question I had on my head while deep on the SessionGuard class - a preexistent Guard shipped by default with the framework - trying to understand what's going on under the hood. I figured that a Guard has the sole responsibility of receiving requests to authenticate a user and to call the configured Provider to fetch that user on the persistent layer.

This means that, for example, when we call Auth::attempt() to attempt a login action, Laravel will defer that call to the same method on the configured Guard. This Guard will, in turn, prepare all it needs to send that request through the related Provider, which may (or may not) return an instance of the Authenticatable interface, the object that represents the authenticated user.

I've also found that we actually have two interfaces for Guards:

  • The Guard interface, referenced in the docs, aimed to a stateless authentication, like APIs;
  • The StatefulGuard interface, that extends the previous Guard interface but also defines how to persist the authentication information in the configured session storage.

The fundamental difference between them: state management. The knowledge of the existence of this StatefulGuard interface pointed me to a good direction of how I could develop a Guard that would persist the JWT in the session that integrates perfectly with the expectations of Laravel itself. I was finally in a good place on understanding the key concepts of the whole authentication system.

Understanding Providers

Laravel's documentation describes Providers as the following:

Providers define how users are retrieved from your persistent storage.

Looking at the two default providers that Laravel supports, EloquentUserProvider and DatabaseUserProvider, I noticed that first returns an instance of Model (an Eloquent model class) and the other handles direct calls to a database table and returns an instance of GenericUser (a slight variation of a Value Object). Both follow the same UserProvider interface.

I concluded that Providers are an implementation of the Repository pattern, and exist only to abstract away how you validate the user's credentials and fetch their data on whatever persistent layer it's stored in.

Actually, that's probably why the user's credentials are passed on inside an array structure: because Laravel doesn't want to be opinionated towards how you authenticate your users. You can use a combination of e-mail and password, or a one-time hash (like magic links sent to an e-mail), or whatever you want to use. It's pretty clever, actually!

But it's basically this... It's inside a Provider that you actually define what data you need to authenticate a user (the credentials) and how you communicate with the persistent layer to get the relevant data. No matter how that's done, a Provider must return an object that implements the Authenticatable interface. This is to ensure that Laravel uses the correct methods when working with that object, directly, like when calling Auth::id().

Making all parts work together nicely

I'm pretty sure that at this point you already have a clearer understanding of the role Guards and Providers take within the Laravel's authentication workflow: Guards receive the request to authenticate a user and pass the credentials to the defined Provider. The Provider, in turn, queries the underlying persistent storage about the relevant user data associated with those credentials and returns it all the way back to the Guard, which makes that information available for usage.

But even if you create all the required classes, you still need to register them within Laravel's Container, so that it knows how to call those classes:

  • To register the Provider, we use the Auth::provider() method and define how the framework should instantiate the custom provider class;
  • To register the Guard, we use the Auth::extend() method and define how the framework should instantiate the custom Guard.

Let me show you exactly what we've done to register the custom Guard and Provider used on the Place Checkup project:

public function boot(): void
{
    Auth::provider('infraspeak-users', static function (Application $app, array $config) {
        return new InfraspeakUserProvider($app->make(InfraspeakApiService::class), $config);
    });

    Auth::extend('infraspeak-jwt', static function (Application $app, $name, array $config) {
        return new JwtSessionGuard(
            Auth::createUserProvider($config['provider']),
            $app->make('session.store')
        );
    });
}

The InfraspeakUserProvider class has all the code to communicate with our API service and validate a user's credentials and returns an instance of the Authenticatable interface. The JwtSessionGuard class has the code that takes that object, from the Provider, and persists it to the session, avoiding new calls being dispatched to the API service while that session is active and valid, protecting our API service from being flooded with authentication requests every time a protected endpoint is being processed.

The very final step in all this work was to update the config/auth.php, and switch the default Guard and Provider used for the web routes:

'guards' => [
    'web' => [
        'driver' => 'infraspeak-jwt',
        'provider' => 'infraspeak-users',
    ],
],

Now we could use Laravel's native classes to authenticate a user and use our API service to serve as the data repository to get that information.