Checking FormRequest's validation rules before authorization in Laravel - php

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!

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
{
}

add $this->getValidatorInstance()->validate(); at beggining of authorize() method

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

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

Related

How do I pass a method with my own constraints in a controller that uses request pattern to display data

Merry Christmass team!
I have a problem trying to figure out how to pass a method with my own constraints in a controller that is bound to request patter paradigm:
Sample Controller Code:
class SampleController
{
protected $model = SampleModel::class;
protected $indexRequest = IndexRequest::class;
}
Request Class
class IndexRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [];
}
Assume I have a method that I want to do something different.Say I want to fetch some data based on some column constraints.
Whats the approach?

Laravel 'required' rule not working on Request

I'm using laravel 5.5
I have a Request that I've built but the required rule is not working correctly.
Route
Route::get('v1/learning_centre/user/{userId}/course/list', 'API\LearningCentre#userCourses');
Controller
public function userCourses(GetUserCourses $request)
{
$courses = User::findOrFail($request->userId)
->courses()
->get();
return new CourseResourceCollection($courses);
}
Request
namespace App\Http\Requests\LearningCentre;
use Illuminate\Foundation\Http\FormRequest;
class GetUserCourses extends FormRequest {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'userId' => 'required|integer'
];
}
/**
* Get the error messages for the defined validation rules.
*
* #return array
*/
public function messages()
{
return [
'userId.required' => 'A User is required',
];
} }
If I turn off the required rule I can get to the controller. If I have the required rule in the request I get a 302. I am passing in a valid userId in phpunit. Without the request rules my code works as intended.
Any ideas?
You should be using route model binding to validate a required GET parameter in this situation, not a FormRequest class, which, as the name should indicate, are intended for form requests.
Your route:
Route::get('v1/learning_centre/user/{user}/course/list', 'API\LearningCentre#userCourses');
Your controller:
public function userCourses(User $user) {
If a user ID is missing (or an invalid one used), your controller will automatically throw a ModelNotFoundException, which Laravel by default returns as a 404.

How to (properly) use form requests + policies + resource routes with dependencies?

My use case is that an user is/owns a company, which has employees.
Using form controllers along with model policies i am trying to figure out what the best/proper way to do it should be.
routes:
Route::resource('company', \App\Http\Controllers\Api\v1\CompanyController::class);
Route::resource('employee', \App\Http\Controllers\Api\v1\EmployeeController::class);
employee store request:
namespace App\Http\Requests;
use App\Models\Employee;
use Illuminate\Foundation\Http\FormRequest;
class EmployeeStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->user()->can('create', Employee::class);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'company_id' => 'required|integer|exists:companies,id'
];
}
}
employee policy:
...
/**
* Determine whether the user can create employees.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
return $user->can('update', Company::find(
app('request')->get('company_id')
));
}
...
So i am not particularly happy in how the policy checks if the user can edit the company the employee will belong to, since this only happens on http, for console/tests this will break.
Then the most logical way to add this check is in the form request's authorize() function, but then you are checking permissions outside the policies, which sounds illogical.
So in short, the question: how & why would you do this using form requests & model policies?
You only need to add the id of the company to the EmployeePolicy#create method, and you will be able to use it outside http
EmployeePolicy
public function create(User $user, $companyId)
{
return $user->can('update', $companyId);
}
EmployeeStoreRequest
public function authorize()
{
return $this->user()->can('create', Employee::class, $this->request->get('company_id'));
}
You can test it outside http with tinker
php artisan tinker
$user = User::find(2); // or whatever user you want to test with
$user->can('create', Employee::class, 3); // 3 = company_id

Request validation allways passes on Laravel using Dingo/Api

I'm using dingo/api package.
Controller:
public function register(RegisterUserRequest $request)
{
dd('a');
}
And for example the email field is required:
<?php namespace App\Http\Requests;
class RegisterUserRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
];
}
}
So I send a request without the email, and still getting the "a" response.
I also tried to extend Dingo\Api\Http\Request instead of App\Http\Request, but still the same.
For Dingo to work at all with the FormRequest, by experience (and from this Issue), you have to use Dingo's Form request i.e Dingo\Api\Http\FormRequest; , so you'll have something similar to:
<?
namespace App\Http\Requests;
use Dingo\Api\Http\FormRequest;
use Symfony\Component\HttpKernel\Exception\HttpException;
class RegisterUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
];
}
// In case you need to customize the authorization response
// although it should give a general '403 Forbidden' error message
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof \Dingo\Api\Http\Request) {
throw new HttpException(403, 'You cannot access this resource'); //not a user?
}
}
}
PS: This is tested on Laravel 5.2.*
Hope it helps :)
According to the Wiki
you must overload the failedValidation and failedAuthorization methods.
These methods must throw one of the above mentioned exceptions and not the response HTTP exceptions that Laravel throws.
If you take a look at Dingo\Api\Http\FormRequest.php, you'll see:
class FormRequest extends IlluminateFormRequest
{
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
*
* #return mixed
*/
protected function failedValidation(Validator $validator)
{
if ($this->container['request'] instanceof Request) {
throw new ValidationHttpException($validator->errors());
}
parent::failedValidation($validator);
}
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof Request) {
throw new HttpException(403);
}
parent::failedAuthorization();
}
}
Hence, you need to change the names of your methods appropriately, and have them throw the appropriate exceptions, instead of returning a boolean.
you need to call the validate function explicitly when you run it under an Dingo API setup, try something like this (for L5.2):
Probably a few extra providers
...
Illuminate\Validation\ValidationServiceProvider::class,
Dingo\Api\Provider\LaravelServiceProvider::class,
...
Aliases
...
'Validator' => Illuminate\Support\Facades\Validator::class,
...
I'm also pretty much sure that you really don't want to use this below as suggested here and there, It will expect form(encoded) input and will also probably fail on CSRF token as it expects it, so it will fail right after validating (form input). But make sure to test behavior with this on/off.
use Dingo\Api\Http\FormRequest;
Make your headers:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Dingo\Api\Exception\ValidationHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/* This can be a tricky one, if you haven't split up your
dingo api from the http endpoint, there are plenty
of validators around in laravel package
*/
use Validator;
Then the actual code (if you adhere to cors standard,
this should be a POST and that commonly translates to a store request)
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function register(RegisterUserRequest $request) {
$validator = Validator::make($request->all(), $this->rules());
if ($validator->fails()) {
$reply = $validator->messages();
return response()->json($reply,428);
};
dd('OK!');
};
...
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
// or/and 'userid' => 'required'
];
}
That will give you back the response you expect from the validator. If you use this with pregenerated forms, it does not need this fix, there the validator will kick in automatically. (not under Dingo Api).
you probably also need these in composer.json
"dingo/api": "1.0.*#dev",
"barryvdh/laravel-cors": "^0.7.1",
This is untested, by heart, it took me 2 days to figure this out but I have a separate namespace for API specific and authenticated with middleware. success

How to force FormRequest return json in Laravel 5.1?

I'm using FormRequest to validate from which is sent in an API call from my smartphone app. So, I want FormRequest alway return json when validation fail.
I saw the following source code of Laravel framework, the default behaviour of FormRequest is return json if reqeust is Ajax or wantJson.
//Illuminate\Foundation\Http\FormRequest class
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->ajax() || $this->wantsJson()) {
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
I knew that I can add Accept= application/json in request header. FormRequest will return json. But I want to provide an easier way to request my API by support json in default without setting any header. So, I tried to find some options to force FormRequest response json in Illuminate\Foundation\Http\FormRequest class. But I didn't find any options which are supported in default.
Solution 1 : Overwrite Request Abstract Class
I tried to overwrite my application request abstract class like followings:
<?php
namespace Laravel5Cg\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\JsonResponse;
abstract class Request extends FormRequest
{
/**
* Force response json type when validation fails
* #var bool
*/
protected $forceJsonResponse = false;
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->forceJsonResponse || $this->ajax() || $this->wantsJson()) {
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
}
I added protected $forceJsonResponse = false; to setting if we need to force response json or not. And, in each FormRequest which is extends from Request abstract class. I set that option.
Eg: I made an StoreBlogPostRequest and set $forceJsoResponse=true for this FormRequest and make it response json.
<?php
namespace Laravel5Cg\Http\Requests;
use Laravel5Cg\Http\Requests\Request;
class StoreBlogPostRequest extends Request
{
/**
* Force response json type when validation fails
* #var bool
*/
protected $forceJsonResponse = true;
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
}
Solution 2: Add an Middleware and force change request header
I build a middleware like followings:
namespace Laravel5Cg\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\HeaderBag;
class AddJsonAcceptHeader
{
/**
* Add Json HTTP_ACCEPT header for an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$request->server->set('HTTP_ACCEPT', 'application/json');
$request->headers = new HeaderBag($request->server->getHeaders());
return $next($request);
}
}
It 's work. But I wonder is this solutions good? And are there any Laravel Way to help me in this situation ?
It boggles my mind why this is so hard to do in Laravel. In the end, based on your idea to override the Request class, I came up with this.
app/Http/Requests/ApiRequest.php
<?php
namespace App\Http\Requests;
class ApiRequest extends Request
{
public function wantsJson()
{
return true;
}
}
Then, in every controller just pass \App\Http\Requests\ApiRequest
public function index(ApiRequest $request)
I know this post is kind of old but I just made a Middleware that replaces the "Accept" header of the request with "application/json". This makes the wantsJson() function return true when used. (This was tested in Laravel 5.2 but I think it works the same in 5.1)
Here's how you implement that :
Create the file app/Http/Middleware/Jsonify.php
namespace App\Http\Middleware;
use Closure;
class Jsonify
{
/**
* Change the Request headers to accept "application/json" first
* in order to make the wantsJson() function return true
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
Add the middleware to your $routeMiddleware table of your app/Http/Kernel.php file
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'jsonify' => \App\Http\Middleware\Jsonify::class
];
Finally use it in your routes.php as you would with any middleware. In my case it looks like this :
Route::group(['prefix' => 'api/v1', 'middleware' => ['jsonify']], function() {
// Routes
});
Based on ZeroOne's response, if you're using Form Request validation you can override the failedValidation method to always return json in case of validation failure.
The good thing about this solution, is that you're not overriding all the responses to return json, but just the validation failures. So for all the other Php exceptions you'll still see the friendly Laravel error page.
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\Response;
class InventoryRequest extends FormRequest
{
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response($validator->errors(), Response::HTTP_UNPROCESSABLE_ENTITY));
}
}
if your request has either X-Request-With: XMLHttpRequest header or accept content type as application/json FormRequest will automatically return a json response containing the errors with a status of 422.
i just override the failedValidation function
protected function failedValidation(Validator $validator)
{
if ($this->wantsJson()) {
throw new HttpResponseException(
Response::error(__('api.validation_error'),
$validator->errors(),
470,
[],
new ValidationException)
);
}
parent::failedValidation($validator);
}
So my custom output sample like below:
{
"error": true,
"message": "Validation Error",
"reference": [
"The device id field is required.",
"The os version field is required.",
"The apps version field is required."
],
}
BTW Response::error dont exist in laravel. Im using macroable to create new method
Response::macro('error', function ($msg = 'Something went wrong', $reference = null, $code = 400, array $headers = [], $exception = null) {
return response()->json(//custom here);
});
I came to this solution (Laravel 9):
throw new ValidationException(
$validator,
new JsonResponse([
'errors' => $validator->errors()->messages(),
], 422),
);

Categories