Implementing Role-Based Authorization with Policy and Gate — Laravel
Authorization in the web application is something to be aware of. If you want to take control of all the permissions that your users have, it’ll be a nightmare if from the beginning you don’t have a proper system for them. Luckily in Laravel, we have several classes that can help us to authorize the users in front-end as well as in back-end. In my previous article here, we have built the database to carry out this need. So, in this article, let’s continue to use our database from my previous article so that we can focus on how implementing this authorization with policy, gate, and middleware. If you haven’t looked at my article before, don’t worry, you can clone the Github here.
- Factory & Seeder Preparation
- User Policy
- Front-end Authorization
- Controller/Backend Authorization
- Adjusting Routes
- Let’s Try it!
Factory & Seeder Preparation
Alright, for testing purpose, we must have the idea what permissions and roles we’ll have and assign those roles to the specific users. I have created the test case below using a mind map, and we’ll stick into it for the entire article.
In my mind map, we have two permissions (create-user & delete-user), four different roles, and four different users. Yeah, you can modify it as you want, but again, we’ll stick into it.
Ok, let’s create the factory and seeder now so that we can try it later. Open the database/factories/ModelFactory.php and modify it into:
Then open the database/seeds/DatabaseSeeder.php.
Don’t forget to set up your database in your .env file and let’s run the migration with seeder.
php artisan migrate:fresh --seed
After it succeeds, you should see some new data appear in your database. Great!
Policy is a class where you can create the authorization rules for your model. For example, let’s say you have a Post model and want to restrict the user for deleting another user post. Then, you can create a Post Policy to make the authorization rules for your Post model.
For this article, because we don’t have any models like post or thread, let’s use the user model. The goal is, we want to restrict some user actions with user policy.
php artisan make:policy UserPolicy --model=User
You should see a new file in your app/Policies/UserPolicy.php. Like always, we must modify it first.
Take a look at the create() function. There, we’re returning a true value when the authenticated user has ‘create-user’ permission. If the value is returning true, then, as you might guess, the user is authorized to do something related to the function policy you use. On the contrary, if the function is returning false, then the user is unauthorized, and Laravel will automatically redirect the user to the unauthorized page with 403 status code.
The logic is more or less same with update() and delete() functions. Those functions must check whether the user has permission to do it or not, and then return a boolean value.
Ok, so we have the policy now, what’s next? Before we continue, we must register our UserPolicy to the app/Providers/AuthServiceProvider.php first.
We are mapping the model with its policy to the policies collection there. And if you see at the boot() function, we’re using Gate::before. It means, before we check the authorization rules from our registered policies, Laravel will run the Gate::before first. As you can see, the logic inside the Gate::before will return true when a user has an ‘admin’ role. If the Gate returns false, then it’ll continue to check the authorization in our registered policies.
We have the policy now. Next, we must apply these rules to the front-end. The basic rules are simple; if a user is not authenticated for some action, then don’t show the button/link to the particular action.
So far we only have one welcome page, let’s just modify it.
As you can see there, we have ‘@can’ directive in our blade file. Of course, this directive is using our user policy, and if the policy is returning false, then Laravel won’t render the components inside the ‘@can’ directive.
Using the ‘@can’ directive is simple, the first argument is the name of the function in our policy, and the second argument is the model itself. If we don’t expect a model inside our function policy, then we must pass the class instead.
With this ‘@can’ directive, we can have the freedom to determine what users see and what they don’t base on their profiles/permissions. Cool right.
Ok, we have protected our front-end, but we also have to protect our back-end. Actually, there are many ways to protect the back-end. You can see those ways if you clone my Github project because I commented on the alternatives there. But for this article, we’ll protect the controller by calling the middleware in the constructor. Let’s open the UserController and modify it.
You can see there; we’re calling the middleware in the constructor. As you might guess, the middleware is using the same ‘can’ structure like in the front-end.
The last thing to do before we try anything, we must reconfigure the routes.
In our new routes, we’ll use the auth()->loginUsingId() function to manually login to a user by the Id because currently, we don’t have any login/register page. So, I think it’s better if we can use this function as they have the same behaviour too.
It’s time for us to try what we’ve done so far. Don’t forget to migrate and seed your database with the seeder we made earlier. For this article, again, we use the mind map (image 1) for our guide.
Let’s login as Maria and see what she will see on the welcome page.
Very Nice! On the welcome page, she sees three links that match with her profile as Admin. You can try clicking all those links, and you’ll see that Maria has all the permissions.
Next, we may try to login using Frank.
As you might guess, Frank only has one link that matches his profile. If you click the update link, he should have the permission, right? But what if we change the URL to http://localhost:8000/create/user/?
Yeah, of course, he doesn’t have the permission, it’s something we expected though. So far, our back-end and front-end succeeded in prohibiting users who don’t have the permission to do that.
Next, let’s login with Jane.
If you think that Jane has the same welcome page as Frank has, it’s true BUT, it’s only in the front-end. If you click the update link there, you can see that Jane has permission to update her own profile. But what if she tries to update a different user like http://localhost:8000/update/user/2?
Jane is a regular user, so she only capable of updating her own profile, not others. It’ll have a different result if you try to login as Frank and try to update different users, he’ll have the permission to do that because we described it in his permissions.
Alright! So far, our authorization is worked! Feel free to play around and try whatever you want. Take a look at my Github project because I left some comments that might be useful for you. Actually, there are more ways to do the authorization check at the back-end, but at least, you got the basic concept. I hope this article can be your useful reference and see you again :-)