Checking FormRequest's validation rules before authorization in Laravel
Asked Answered
W

5

5

I'm implementing an API in Laravel using JSON:API specification.

In it I have a resource, let's call it Ponds, with many-to-many relationships with another resource, let's call it Ducks.

According to JSON:API specs in order to remove such relationship i should use DELETE /ponds/{id}/relationships/ducks endpoint, with request of following body:

{
    "data": [
        { "type": "ducks", "id": "123" },
        { "type": "ducks", "id": "987" }
    ]
}

This is handled by PondRemoveDucksRequest, which looks as follows:

<?php
...
class PondRemoveDucksRequest extends FormRequest
{
    public function authorize() 
    {
        return $this->allDucksAreRemovableByUser();
    }

    public function rules()
    {
        return [
            "data.*.type" => "required|in:ducks",
            "data.*.id" => "required|string|min:1"
        ];
    }

    protected function allDucksAreRemovableByUser(): bool
    {
        // Here goes the somewhat complex logic determining if the user is authorized 
        // to remove each and every relationship passed in the data array.
    }
}

The problem is that if I send a body such as:

{
    "data": [
        { "type": "ducks", "id": "123" },
        { "type": "ducks" }
    ]
}

, I get a 500, because the authorization check is triggered first and it relies on ids being present in each item of the array. Ideally I'd like to get a 422 error with a standard message from the rules validation.

Quick fix I see is to add the id presence check in the allDucksAreRemovableByUser() method, but this seems somewhat hacky.

Is there any better way to have the validation rules checked first, and only then proceed to authorization part?

Thanks in advance!

Weintrob answered 11/3, 2019 at 14:37 Comment(7)
Could you post where you are calling the validation rules please?Cadenza
@Cadenza I'm not sure if I get your question. As far as I understand the validation rules are checked automatically when the request is used in the controller method's parameters.Weintrob
In your controller when you retrieve the request, could you dd $request->validated(); just to see if the request passes the validation? Because if it is passing, then there is an issue with the validation, and if it fails then you need to reject that request :)Cadenza
@Cadenza The rules validation itself seems to work, since it returns 422 if a wrong "type" is provided in one of the removed items. I will check if adding the $request->validated(); helps with the order of rules vs. authorization checks though, thanks!Weintrob
any luck in solving it?Cadenza
@Cadenza Adding the $request->validated() call in the controller made no difference. I assume that the FormRequest's authorization logic is executed before any code within the controller methods. What helped was adding $this->getValidatorInstance()->validated(); at the beginning of the authorize() method. getValidatorInstance() is required, because when the authorization logic is executed, the validator is not yet instantiated. This way I got standard error messages and validation based on rules(). Still looks a bit messy though, I'm thinking about moving it to middleware.Weintrob
IMO validating before authorization sounds wrong. Returning validation errors to an user that is not allowed to see that resource at all, might cause information disclosure. I would recommend to guard in your authorization logic against malformed requests.Singlecross
H
5

You can validate the request manually in the authorize method

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class SomeKindOfRequest extends FormRequest
{
    function authorize(): bool
    {
        $this->getValidatorInstance()->validate();

        // perform your authorization afterwards
    }
}
Heerlen answered 22/11, 2019 at 16:5 Comment(0)
A
5

1 - Create abstract class called "FormRequest" inside App\Requests directory and override the validateResolved() method:


<?php

namespace App\Http\Requests;

use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;

abstract class FormRequest extends BaseFormRequest
{

    /**
     * Validate the class instance.
     *
     * @return void
     * @throws AuthorizationException
     * @throws ValidationException
     */
    public function validateResolved()
    {

        $validator = $this->getValidatorInstance();

        if ($validator->fails())
        {
            $this->failedValidation($validator);
        }

        if (!$this->passesAuthorization())
        {
            $this->failedAuthorization();
        }
    }

}

2 - Extend your FormRequests with custom FormRequest

<?php

namespace App\Http\Requests\Orders;

use App\Http\Requests\FormRequest;

class StoreOrderRequest extends FormRequest
{



}


Adrell answered 5/8, 2020 at 18:39 Comment(0)
O
1

The most cleanest solution I found to solve it was by creating a small trait for the FormRequest and use it anytime you want to run validation before the authorization, Check the example bellow:

<?php

namespace App\Http\Requests\Traits;

/**
 *  This trait to run the authorize after a valid validation
 */
trait AuthorizesAfterValidation
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     *  Set the logic after the validation
     * 
     * @param $validator
     * @return void
     */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            if (! $validator->failed() && ! $this->authorizeValidated()) {
                $this->failedAuthorization();
            }
        });
    }

    /**
     *  Define the abstract method to run the logic.
     * 
     * @return void
     */
    abstract public function authorizeValidated();
}

Then in your request class:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Traits\AuthorizesAfterValidation;

class SomeKindOfRequest extends FormRequest
{
    use AuthorizesAfterValidation;

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorizeValidated()
    {
        return true; // <---- Set your authorization logic here
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

Source https://github.com/laravel/framework/issues/27808#issuecomment-470394076

Overrefinement answered 9/11, 2021 at 6:45 Comment(0)
R
0

Here is a slightly different approach than what you are attempting, but it may accomplish the desired outcome for you.

If you are trying to validate whether the given duck id belongs to the user, this can be done in the rule itself as follows:

"data.*.id" => "exists:ducks,id,user_id,".Auth::user()->id

This rule asks if a record exists in the ducks table which matches the id and where the user_id is the current logged in user_id.

If you chain it to your existing rules (required|string|min:1), using 'bail', then it wouldn't run the query unless it had passed the other three rules first:

"data.*.id" => "bail|required|string|min:1|exists:ducks,id,user_id,".Auth::user()->id
Reserve answered 25/4, 2019 at 15:23 Comment(1)
That looks like a valid solution in case if the authorization logic relies only on the relationship between the user and the pond. Unfortunately in my case, there's more to it - stuff needs to be determined when it comes to user's previleges, relation to pond's parent entity (can't find a nice analogy at the moment ;)) etc. Thanks anyway, I think this can be helpful in a lot of other cases.Weintrob
D
0

method 1 :

You can use this function & call it top of your authorize function. It run rules first :

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
protected function validateBeforeAuthorize()
{
        $validator = Validator::make($this->all(), $this->rules());
    
        if ($validator->fails()) {
            throw new ValidationException($validator);
        }
 }

like this :

public function authorize(): bool
{
  $this->validateBeforeAuthorize();
  //rest of your code
}

method 2 :

or you can add this to your request file. It changes the order of functions in the request file and executes the rules function first

class CustomRequest extends FormRequest
{
  public function validateResolved(): void
    {

        $validator = $this->getValidatorInstance();

        if ($validator->fails())
        {
            $this->failedValidation($validator);
        }

        if (!$this->passesAuthorization())
        {
            $this->failedAuthorization();
        }
    }

    //rest of your code
}
Dust answered 23/8 at 7:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.