Laravel has one of the most common features a website will need baked directly into it, authentication with persistent users, in this case, stored in any of the supported databases (MySQL, SQLite etc.). This stuff is amazing when you consider you can setup a web application near instantly and have user authentication with forgotten passwords and all the trimmings in a matter of minutes. What happens though when you need something a bit more… complex?
For instance, what if you need multiple user types who have their own authentication portals. These different kinds of users could be Customers, Sellers and Administrators. Such a mechanism can get awfully confusing very fast, I know because I’ve been there. We often preplan what our database looks like but don’t structure what our authentication flows and mechanisms look like.
In this article I hope to break down and explain how to handle these scenarios.
Guards in Laravel is the mechanism with which your application can know if someone or even something is authenticated or not. When we look at the default install of Laravel we typically see one guard which is web. When a visitor is authenticated with this guard, any use of the auth middleware will allow the user through to view the page, this is because out of the box the default guard is always web. If a person is browsing and not authenticated at any point then they are referred to as a guest within that guard.
Typically when an additional guard is added to a web application, it’s to provide an API with a different authentication mechanism such as user tokens. This default guard doesn’t have to be the only web guard in your application though. In fact we can have a Guard for different user types, even ones who don’t authenticate using traditional usernames and passwords.
To demonstrate creating a new Authenticatable we’re going to use an example of a page with which a customer for an Order can be authenticated. The customer can only authenticate with the application using a signed URL and once authenticated they can perform additional actions like cancelling the order.
First we create a new model with:
php artisan make:model Order
Now, we need to modify the Order model found in app/Models/Order.php with a few interfaces and traits. This with satisfy the Order model to be useable with the guards and the Eloquent provider type.
Notice that compared to the out of the box User model we could have simply extended the framework’s User class, but because we don’t plan on using passwords, we’re going to ignore our model being able to reset their password.
Once this is done we need to add our guard to the auth config in configs/auth.php. Because we’re using a different model as well, we need to implement an addition provider which we’re going to call the orders provider which will be used by the customer guard.
That’s it, our new guard is now authenticated but we need a mechanism to authenticate visitors without putting in a password.
Technically yes, Authenticatables need passwords as it’s a part of the Illuminate\Contracts\Auth\Authenticatable interface which requires an implementation for getAuthPassword(). In the case of the example previously we’ve used the Authenticatable trait to provide the implementation. The code for this though only gets used if we try to use the attempt method of the guard and we’re not going to use this.
In this scenario we have no plans to ever authenticate our Orders by emails and passwords so we don’t have to worry about this. Instead we’ll simply create a new Middleware that will handle the authentication from a signed url which only our application can generate for the Seller to give to the Customer.
First we’ll set up an example route for our Orders in routes/web.php.
Notice we’ve added a auth.signed middleware. This does not exist yet so we’ll have to create one and add it to the Http Kernel. We can create the middleware with the command:
php artisan make:middleware AuthenticateWhenRequestIsSigned
This will create the file app/Http/Middleware/AuthenticateWhenRequestIsSigned.php which we can edit. We’ll add two parameters to the handle method which will be the parameter name to use from the route and the guard we want to authenticate with. The code for the handle method then is pretty simple, if the request is signed, use the ID value from the Order parameter to authenticate the customer.
Now we’ve created the middleware we need to register it in the kernel.
Doing so won’t make the middleware work as as we’re also using the auth middleware for our route, this means that the auth.signed middleware will never get to execute as the auth middleware has priority and will terminate the request before the signing middleware gets a chance to authenticate the customer.
To resolve this we only need add an extra array to the kernel to set the priority of the auth signed middleware to run after the session start middleware.
We can do this by adding a middlewarePriority property to our Kernel, overriding the parent Kernel. Doing so before the AuthenticatesRequests middleware and the StartSession middleware means that the middleware can authenticate the customer when a valid signature is provided in the URL.
Now whenever a visitor lands on that page, using a url with a valid signature, they’ll be authenticated with our customer guard and be able to revisit the url without the signature until their session times out. There is still a problem with this though, any customer that does this will also be able to view not just their order, but any order by simply changing the id in the URL. Bearing in mind here that authentication is not authorization which means to secure the other orders from a customer we need to add some authorization.
This is a fairly simple procedure. We now only require a policy, but in this instance we’ll need to use the guard argument as part of the policy make command. This will then allow us to generate most of the code we’ll need.
php artisan make:policy --guard customer --model App/Models/Order CustomerOrderPolicy
Now because the model and the authenticable match we’ll need to rename the parameters on several of the methods and assign a return value for the methods which will allow an order to only view and update itself. We need to go ahead an edit app/Policies/CustomerOrderPolicy.php. We implement the methods for updating and viewing a single order, the rest can return false.
Once we’ve done this we only need register the policy and add the middleware to the route. Now when an authenticated customer tries to access any orders but their own, they’ll be unsuccessful. With this we’ve secured the application with authentication and authorisation for customers.
Now we apply the policy of viewing an order to our route to view the order.
Having a guard called customers only really makes sense if we also have a guard called sellers who will still authenticate with emails and passwords to generate the orders for customers. We already have the web guard, but this isn’t really for all web user but instead it’s for sellers so we’ll name it accordingly.
Renaming the default guard can become tricky, especially as other middleware and packages like Laravel Sanctum and Fortify will use the web guard by name. Luckily both packages have configuration options available to change this painlessly.
First we have to edit configs/auth.php to have a guard named seller. We then also need to update the default to reflect the name change.
If we’re also using Fortify and Sanctum then each config will require a guard value to be set which will configure the guard for these packages. After which we should be good to go. You’ll also have to update an routes using auth:web to now use auth:seller instead.
Working with Guards can be a bit confusing at first, it definitely requires some exploration before making long term decisions. I’ve worked on multiple projects where separating the guards has been both beneficial and a burden. Often the best way to handle this is to build a quick prototype of how you’ll handle some of that separation. Often using a Gate can be a better option to decide if a visitor can access a particular part of a website.
I’ve been fairly brief with all the steps in this article but if you wish to experiment more or see how this workflow works in action you can clone and setup the code from the github repository I’ve setup. I’ve also included a test in the demo code that you can use should you want to experiment further.
I’m Peter Fox, a software developer in the UK who works with Laravel among other things. Thank you for reading my article, I’ve got several more on both medium and dev.to. If you want to know more about me, head over to https://www.peterfox.me. I’m also now also Sponsorable on GitHub. If you’d like to encourage me to write more articles like this please do consider dropping a small one off donation.