Cover Image
#laravel #security #testing

Protect user uploads with authorization layer in laravel

~ 4 MINS READ
1st Dec 2021
Mohammed Omer By Mohammed Omer

Sometimes you want to store users' files and make them accessible only to the file's owner or the site's admin (or any other authorization logic).

In this tutorial, we will be creating two routes to store and retrieve files with an authorization layer. I will assume you have your authentication system in place.

What we will be working on

Let's say we want every user in our system to be able to upload their resume (a pdf file) to their profile. And we only want the owner of this resume and admins to have access to it.

You should never store private files in the public directory, otherwise, everyone (even non-authenticated users) will have access to those files.

So we will be using storage/app/resumes to store all users' resumes in the form of storage/app/resumes/{id}/resume.pdf so that every user has a unique directory to store their resume.

Creating an endpoint to store the resume

We first define a route to handle the file upload.

Route::post('/resume', [UserResumeController::class, 'store'])
    ->middleware('auth')
    ->name('users.resume.store');

Note: a user must be authenticated to be able to upload a resume, hence the auth middleware.

Then, we create a store method in UserResumeController to handle the above route:

public function store(Request $request)
{
    $request->validate([
        'resume' => 'file|mimetypes:application/pdf|max:4000'
    ]);
    $request->file('resume')->store('resumes/' . auth()->user()->id . '/resume.pdf');
    return back()->with('message', 'Resume uploaded successfully');
}

Accessing the files

At this point, users can upload their resumes. Now to the part of making them accessible only by the user who created them and admin users.

Create an endpoint to show resumes:

Route::get('/resumes/{user}', [UserResumeController::class, 'show'])
    ->middleware('auth')
    ->name('users.resume.show');

Create a show method in UserResumeController to authorize users and fetch the appropriate resume:

public function show(User $user)
{
    if (auth()->user()->is_admin || $user->is(auth()->user())) {
        return Storage::download('resumes/' . $user->id . '/resume.pdf');
    }
    abort(401);
}

Now only admins and owners of the resume can view that resume.

Of course, you can change the way you store files ( storing the path of the file in the database or creating another table if there are multiple files per user ). This tutorial showed only the basics of protecting user uploads.

Bonus:

As I was writing this post, I used tests to quickly validate my code instead of creating blade views and opening the browser to test if it works. This is a nice way to decouple backend logic from front-end and quickly add some functionality without leaving your editor or using a REST api client like postman. This is the contents of the BIG test that validates the above code:

public function setUp(): void
{
    parent::setUp();
    Artisan::call('migrate');
}

/** @test */
public function test_only_owners_of_a_resume_and_admin_users_can_access_the_uploaded_resume()
{
    Storage::fake('local');
    $admin = User::factory()->create(['is_admin' => 1]);
    $firstUser = User::factory()->create();
    $secondUser = User::factory()->create();
    $this->actingAs($firstUser);

    Storage::assertMissing('resumes/' . $firstUser->id . '/resume.pdf');
    $this->post(route('users.resume.store'), [
        'resume' => UploadedFile::fake()->create('hello.pdf', 2000, 'application/pdf')
    ])->assertRedirect();

    Storage::assertExists('resumes/' . $firstUser->id . '/resume.pdf');

    $this->get(route('users.resume.show', $firstUser->id))
        ->assertStatus(200)
        ->assertDownload('resume.pdf');

    $this->be($admin);
    $this->get(route('users.resume.show', $firstUser->id))
        ->assertStatus(200)
        ->assertDownload('resume.pdf');

    $this->be($secondUser);
    $this->get(route('users.resume.show', $firstUser->id))
        ->assertStatus(401);
}

Thanks for completing this tutorial. let me know if you want to learn more about testing laravel apps


If you liked this post consider sharing it :

You may also like

stopwatch6 MINS 
cover
By Mohammed Omer | 15th Jan 2022
#laravel #testing
stopwatch4 MINS 
cover
By Mohammed Omer | 21st Dec 2021
#laravel #performance
stopwatch3 MINS 
cover
By Mohammed Omer | 29th Jan 2022
#security #database
stopwatch14 MINS 
cover
By Mohammed Omer | 22nd Jan 2022
#laravel #linux #nginx
stopwatch6 MINS 
cover
By Mohammed Omer | 15th Mar 2022
#laravel #tooling #php
stopwatch2 MINS 
cover
By Mohammed Omer | 27th Mar 2022
#laravel #productivity #shorts 🔥 #tooling
stopwatch3 MINS 
cover
By Mohammed Omer | 7th Dec 2021
#laravel #linux