The Definitive Guide for Implementing Authorization in Laravel
- Share:
Introduction
Authorization provides a mechanism for determining what resources a user can access based on their identity and permissions, which is a critical part of every application's security. In a nutshell, when a user requests a specific resource, the system first must verify the user’s identity. Then, it checks to see if they have the right permission to access this resource and allows or denies access based on the permission check.
For example, in a school management system, a teacher needs access to students’ records like grades, lesson plans, logbooks, and progress registers. These records are something you might want students viewing, but definitely not editing. By implementing fine-grained permissions, you can ensure that the right people have the can access the right records.
Laravel provides a simple, organized, built-in approach (gates and policies) for managing authorization and permissions. However, this built-in functionality has many limitations when handling sensitive user data, thus requiring a more fine-grained and customizable authorization-as-a-service tool like Permit.
This article will demonstrate how to implement fine-grained authorization in Laravel using Permit.io, a tool for managing permissions.
Sample Application: A School Management System
A school management application is a good way to demonstrate how authorization works, how Laravel’s built-in approach to handling authorization may not always be sufficient, and why you should consider using a customizable, fine-grained authorization tool.
A school management system handles data such as student’s attendance, grades, report cards, class schedule, etc. Within the system, students, teachers, and the principal are given access to resources based on their defined roles and permissions. For example, the teachers in a school should have access to all students’ information, such as:
- Academic Information: Course enrollments, transcripts, grades, attendance records, progress reports, etc.
- Demographic information: Name, date of birth, emergency contact information, parents’ information, etc.
- Health information (in case of an emergency): Allergy information, medical history, etc.
- Disciplinary records: Documented incidents of misconduct, disciplinary actions, etc.
Students may only need to access (view) specific details, such as their own grades and report cards. Class teachers' access may also be restricted only to students in their class.
Consider the following Laravel endpoint for retrieving students’ information based on the user’s permission.
namespace App\\Http\\Controllers;
use Illuminate\\Http\\JsonResponse;
use Illuminate\\Http\\Request;
use App\\Models\\Student;
use Gate;
class StudentDetail extends Controller
{
public function show(Request $request, $studentId)
{
// Retrieve student information from the database
$student = Student::findOrFail($studentId);
// Check if the user has permission to view this student's information
if (! Gate::allows('view-student', $student)) {
return response()->json(['error' => 'Permission denied'], 403);
}
// Serialize student data (assuming you have a serialization method)
$serializedStudent = $this->serializeStudent($student); // Replace with your serialization logic
return response()->json($serializedStudent);
}
// Add a method to handle student data serialization (replace with your actual logic)
private function serializeStudent(Student $student)
{
// Implement your logic to convert student data to the desired format (e.g., JSON)
// This example returns an empty array for demonstration purposes
return [];
}
}
The StudentDetail
does the following:
- Handles GET request.
- Retrieves a student’s data from the database using the
studentId
. - Throws a 403 error if a user has no permission to access student data.
- If the user has permission, it serializes the student’s data and returns a response.
Understanding Laravel’s Built-in Authorization
Laravel provides two ways to handle authorization - Gates and Policies. Gates and policies both have their strengths.
To enforce permission in Laravel, you can create gates or policies to grant or deny access to a given resource. In this example, let's use gates to grant or deny user access based on their role (RBAC - Role Based Access Control).
app/Providers/AuthServiceProvider.php
public function boot()
{
$this->policies(Model::class, ModelPolicy::class);
// Teacher Gate
Gate::define('view-student', function ($user, $student) {
return $user->hasRole('teacher');
});
// Student Gate (view only their own record)
Gate::define('view-own-record', function ($user, $student) {
return $user->hasRole('student') && $student->user_id === $user->id;
});
// Gates for other permissions (create, update, delete) following similar logic
Gate::define('create-student', function ($user) {
return $user->hasRole('teacher');
});
Gate::define('update-student', function ($user, $student) {
return $user->hasRole('teacher');
});
Gate::define('delete-student', function ($user, $student) {
return $user->hasRole('teacher');
});
}
The gates defined above grant view, create, update, and delete access to teachers but restrict student access to view-only, utilizing a user’s role to determine what permission they have.
The following controller code enforces the above permissions using the authorize
method.
public function show(Request $request, $studentId)
{
$this->authorize('view-student', Student::class);
$student = Student::findOrFail($studentId);
}
While the above method of implementing authorization might solve the problem, it does so only partially, as Laravel’s built-in authorization functions cannot handle the combination of user attributes (ABAC - Attribute Based Access Control) and context. That’s where Permit.io comes in.
Permit.io helps us handle the limitations of Laravel’s built-in authorization by considering additional, more fine-grained factors as part of the authorization decision, such as resource attributes, user attributes, and environmental attributes. Decoupling authorization logic from application-business logic also makes the maintenance of our permission system cleaner, more manageable, and thus more secure.
Fine-Grained Authorization
Fine-grained authorization allows you to define stricter permission beyond the common role-based access control (RBAC) using factors such as:
- Resource Attributes: Describe the characteristics of the resource itself, like ownership, class allocation, or test results.
- User Attributes: This describes the user’s characteristics such as grade, allergies information, or location.
- Action Granularity: This specifies the action being performed, such as edit, create, view, or delete.
- Contextual Information: This includes factors like IP address, device type, location, or other specific conditions for further refining access control.
The above factors can help create a more granular authorization system to keep your application more secure.
What are ABAC and ReBAC
Let's take a look at what ABAC and ReBAC are about.
- Attribute-Based Access Control (ABAC): ABAC is an authorization model that evaluates the characteristics of the various components involved in the access request (user, resource, and environment) to determine whether access should be granted or denied. The attributes to be evaluated include location, user roles, resource type, device type, etc. The ABAC system uses these attributes to specify who can perform what actions. For example, we can write an ABAC policy that grants a grade 7 teacher access to data of students in grade 7.
- Relationship-Based Access Control (ReBAC): The ReBAC model uses relationships between entities to manage access permissions. You can define policies that grant or deny resource requests based on the user’s relationship to that resource or other entities. In the case of our school management system, we can grant access to a student’s records based on teacher-student relationship (e.g. teacher assigned to a particular class, student enrolled for a course taught by a particular teacher, etc.).
Check out this guide for a detailed explanation of RBAC, ABAC, and ReBAC. You can also learn more about the pros and cons of ABAC and ReBAC here.
The diagram above represents the authorization modeling for our school management system. Here is a breakdown of the relationships and entities.
Entities:
Entities are subjects or objects involved in an access control decision. In our case, we have the following entities:
- User: This represents the individual making a specific request. The user entity has a relationship with role, grade, and students.
- Role: Roles are predefined categories of users with specific permissions. In our school management system, roles can include teacher, principal, student, etc.
- Grade: Grade represents the class within the school where a student belongs or where a teacher teaches.
- Student: This represents an individual studying in the school. Students belong to specific grades.
Relationships:
Relationships indicate the connection between entities.
- Has Role: Indicates assigned one or more roles.
- Grade: Indicates the class a user teaches.
- Teaches: Represents the relationship between a user and a student. It suggests that the user teaches a particular student or group of students.
- Belongs To: Indicates the grade a student belongs to.
Permit.io Policy
Permit helps us define and represent the permission policies implemented in our school management system. The defined policy applies to users, roles, students, and grades, as shown in the diagram above. Permit operates on the following planes:
- Control Plane: In this plane, you can define your authorization policies using various available entities like user IDs, actions, attributes, and other access control conditions. In our example, we’d define ABAC policies for viewing student records based on grade and ReBAC policies for teacher-student relationships.
- Data Plane: The actual data of entities is stored in the data plane to make authorization decisions. This includes role definitions, user attributes, and resource attributes.
- Enforcement Plane: At this plane, authorization requests are evaluated. The system checks the user’s request against the defined policies to determine access to a resource.
Implementing ABAC with Permit.io
There are two ways to implement an authorization model with Permit.io - UI and SDK/API.
We are going to explore both ways. In this section, we will implement ABAC using the Permit’s UI and enforce it with the API.
Let's go through how to implement ABAC in our Laravel-based school management system using Permit.io. We will create a policy that grants or denies access to a user based on some attributes.
To follow along, you need:
- Some Knowledge of PHP and Laravel.
Step 1: Log in to Permit.io
Log in to the Permit Web application and create your workspace
Step 2: Create a new resource
By default, you have a project created for you that includes two environments: Development and Production. Define and test your policies in the development environment before deploying them to production.
Start by defining your resources in the Development environment. Let's create a new resource for student_record
. Click on Resources and select Create a Resource.
Let's add an additional attribute called grade
to the resource.
Step 3: Define new user attributes
Next, let’s define some user attributes and assign them a user role.
Step 4: Create role attributes
Next we will add role attributes to our users. We need to create new attributes named teacher
and student
. These attributes will represent the roles of a user.
- Click on Settings
- Inside the Role Attributes tab, click Add Attribute. Then, add the attributes for
teacher
andstudent
.
Step 5: Create a new ABAC rules
Let’s create an ABAC rule that allows a Grade-9 teacher to view student records in the same grade. We’ll name the rule grade9-teachers
. Navigate to the ABAC rule tab, create a user set and define the following conditions:
- User Grade: Grade-9
- User Role:
teacher
=True
Following the same steps, let's also define conditions for grade 9 students
. Define the following conditions:
- Student Grade: Grade-9
- Student Role:
student
=True
Step 6: Create a new policy
Policies allow us to specify which users can perform which actions. In our case, we’ll allow all actions with the role teacher
and the grade Grade-9
to be able to view the student_record
resource. For the student
role and the Grade-9
th grade, we’ll enable only read action.
We have finished defining our policies in the Permit.io web interface. Next, let's enforce them in our Laravel application.
Enforcing ABAC Policies with Laravel Using Permit API
Permit’s API allows you to verify permission and enforce ABAC policies in your application.
Here's how you can enforce your policies with Laravel:
<?php
namespace App\\Http\\Controllers;
use App\\Http\\Requests\\StoreStudentRequest;
use App\\Http\\Requests\\UpdateStudentRequest;
use App\\Models\\Student;
use GuzzleHttp\\Client;
use Illuminate\\Http\\Request;
class StudentController extends Controller
{
private $permitKey;
private $projectId;
private $envId;
private $client;
public function __construct()
{
$this->permitKey = config('permit.key');
$this->projectId = config('permit.project_id');
$this->envId = config('permit.env_id');
$this->client = new Client();
}
// Create student
public function store(StoreStudentRequest $request)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'create', 'student')) {
return response()->json(['error' => 'Forbidden'], 403);
}
$student = Student::create($request->validated());
return response()->json($student, 201);
}
// View student
public function show(Student $student)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'read', 'student')) {
return response()->json(['error' => 'Forbidden'], 403);
}
return response()->json($student, 200);
}
// Update student
public function update(UpdateStudentRequest $request, Student $student)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'update', 'student')) {
return response()->json(['error' => 'Forbidden'], 403);
}
$student->update($request->validated());
return response()->json($student, 200);
}
// Delete student
public function destroy(Student $student)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'delete', 'student')) {
return response()->json(['error' => 'Forbidden'], 403);
}
$student->delete();
return response()->json(null, 204);
}
// Function to check permissions
private function checkPermission($email, $action, $resource)
{
$url = "<http://localhost:7766/allowed>";
$payload = [
"user" => $email,
"action" => $action,
"resource" => [
"type" => $resource,
"tenant" => $this->projectId, // Replace with appropriate tenant ID if different
],
"context" => new \\stdClass(), // Empty context object
];
try {
$response = $this->client->post($url, [
'headers' => [
'Authorization' => "Bearer {$this->permitKey}",
'Content-Type' => 'application/json',
],
'json' => $payload,
]);
$result = json_decode($response->getBody()->getContents(), true);
return $result
} catch (\\Exception $e) {
return false;
}
}
}
The checkPermission
function in the code snippet above checks the user's permission to perform the specified action by making a POST request to our localhost PDP running on port 7766
. We'll cover running your PDP container with Docker later. The checkPermission
function throws an error if the user does not have the permission to perform such action on the resource. Otherwise, it returns an OK
with the status code 200
and allows the request.
Implementing ReBAC with Permit.io UI
Permit.io also allows you to implement ReBAC in your Laravel application. To enforce ReBAC, first define relationships between entities and then create rules based on those relationships.
The following are the steps to implement ReBAC in Permit.io.
Step 1: Define Relationships:
Define the relationship between users and resources. For this example, we need to define a relationship between a teacher and a student, where a teacher teaches a student.
Let’s edit our student_record
and add two new ReBAC options: teacher
and student
. We will link these to related roles later.
Let’s create an additional resource, s1
, which derives from the main student_record
resource. This resource represents an individual student’s record. We’ll also specify teacher
and student
options for this resource.
To create a relationship between a teacher and a student, we must specify that student_record
is a parent of s1
.
Return to the student_record
resource and add that student_record
is parent of s1
.
Step 2: Create ReBAC rules
We must define our ReBAC rules for granting access based on shared relationships. In our example, we want to allow a teacher to view a student’s record if they are the student’s grade teacher.
Now, let's navigate to the roles tab. There, we’ll see the teacher
and student
roles we created earlier for each resource. We can now link the student_record
resource to the teacher
role and the s1
resource to the student
role.
Do the same thing for the student
role and s1
resource.
Our ReBAC rules are now defined. Lets go over and enforce it inside our Laravel application.
Enforcing ReBAC Policies with Laravel Using Permit API
Let's enforce the ReBAC policies we defined in the Permit.io web interface using the Permit API. Permit provides a flexible API for making authorization decisions in your application.
Here's how you can enforce your ReBAC policies in Laravel with Permit API:
<?php
namespace App\\Http\\Controllers;
use App\\Http\\Requests\\StoreStudentRequest;
use App\\Http\\Requests\\UpdateStudentRequest;
use App\\Models\\Student;
use GuzzleHttp\\Client;
class StudentController extends Controller
{
private $permitKey;
private $projectId;
private $envId;
public function __construct()
{
$this->permitKey = config('permit.key');
$this->projectId = config('permit.project_id');
$this->envId = config('permit.env_id');
}
public function index()
{
$students = Student::all();
$user = auth()->user();
if (!$this->checkPermission($user->email, 'read', 'student')) {
return response()->json(['error' => 'Forbidden'], 403);
}
return response()->json($students, 200);
}
public function store(StoreStudentRequest $request)
{
$student = Student::create($request->validated());
$user = auth()->user();
if (!$this->checkPermission($user->email, 'create', 'student')) {
return response()->json(['error' => 'Forbidden'], 403);
}
return response()->json($student, 201);
}
public function show(Student $student)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'read', 'student', $student->id)) {
return response()->json(['error' => 'Forbidden'], 403);
}
return response()->json($student, 200);
}
public function update(UpdateStudentRequest $request, Student $student)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'update', 'student', $student->id)) {
return response()->json(['error' => 'Forbidden'], 403);
}
$student->update($request->validated());
return response()->json($student, 200);
}
public function destroy(Student $student)
{
$user = auth()->user();
if (!$this->checkPermission($user->email, 'delete', 'student', $student->id)) {
return response()->json(['error' => 'Forbidden'], 403);
}
$student->delete();
return response()->json(null, 204);
}
private function checkPermission($email, $action, $resource)
{
$url = "<http://localhost:7766/allowed>";
$payload = [
"user" => $email,
"action" => $action,
"resource" => [
"type" => $resource,
"tenant" => $this->projectId, // Replace with appropriate tenant ID if different
],
"context" => new \\stdClass(), // Empty context object
];
try {
$response = $this->client->post($url, [
'headers' => [
'Authorization' => "Bearer {$this->permitKey}",
'Content-Type' => 'application/json',
],
'json' => $payload,
]);
$result = json_decode($response->getBody()->getContents(), true);
return $result
} catch (\\Exception $e) {
return false;
}
}
}
In the code snippet above, the checkPermission
function calls our local PDP endpoint to determine if a user has permission to perform the specified action. The update
function checks if the user can perform the update action. The show
function checks if a user has permission to view resources. The destroy
function checks if the user has permission to delete a user
, and the store
function checks if the user has permission to create a new user. Each of these functions enforces access control through the Permit API.
Running the Application
To run the Laravel application, clone the source code and run the following code.
composer install
php artisan install:api
php artisan migrate
php artisan serve
Your Laravel app should run at localhost:8000.
Remember to add your Permit.io PDP URL and API_KEY
to the .env file.
Running the Permit.io PDP Microservice container is a quicker way to develop and test your app. Let’s set it up.
- Install Docker on your machine and pull the PDP Microservice container:
docker pull permitio/pdp-v2:latest
- Run the container using the code below. Replace
<YOUR_API_KEY>
with the secret key you retrieved from your dashboard
docker run -it -p 7766:7000 --env PDP_DEBUG=True --env PDP_API_KEY=<YOUR_API_KEY> permitio/pdp-v2:latest
After running the command above, your Permit.io PDP Microservice should be live at http://localhost:7766.
Self-Assigned Roles and Permissions
In a dynamic environment, you want to allow users to assign themselves certain roles, subject to the approval of a party with higher permissions. The user’s role assignment request is logged and sent for review. An authorized party reviews the request to grant or deny the user’s request based on their current role and the organization’s policies. If approved, the user’s permissions and assigned roles are updated. The user also gets a notification when their request is approved or denied. This process is known as approval workflow.
Here is an example use case: A teacher may need temporary access to student records (e.g. health information) outside their class (grade) during a school emergency. The teacher can request the necessary permissions without waiting for an admin. The request triggers the set approval workflow, reducing manual inputs from administrators.
Permit’s API allows you to assign roles to users upon approval by an admin. The following code gives you an idea of how to create an approval request using Permit API.
To create an access request, make a POST request to the access_requests
endpoint and pass the access_request_details
.
curl '<https://api.permit.io/v2/elements/{proj_id}/{env_id}/config/{elements_config_id}/access_requests>' \\
-data-raw
{
"access_request_details": {
"tenant": "34f5c98e-f430-457b-a812-92637d0c6fd0", // replace with tenant
"resource": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", //replace with resource
"resource_instance": "2d98d9f8-e1b7-4f1d-baad-2edbf6fa6c66", //replace with resource instance
"role": "ac4e70c8-d5be-48af-93eb-760f58fc91a9" //replace with role
},
"reason": "Need access to medical records of a student to another grade",
} \\
Permit Redoc gives you more information about approval workflow.
Conclusion
Fine-grained authorization is vital to application security. Attribute-Based Access Control (ABAC), Relationship-Based Access Control (ReBAC), and Role-Based Access Control (RBAC) are authorization models that allow you to define access control policies for your application and prevent unauthorized access to resources. These models help you enhance the security of your application and protect sensitive user data.
Permit.io is a full-stack authorization-as-a-service tool that integrates seamlessly with your existing workflows and helps you securely manage permissions. It supports any authorization model - RBAC, ABAC, and ReBAC, and provides a flexible and straightforward approach to implementing fine-grained permissions in your applications.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker