Laravel Validation and Custom Rules

Hi, I’m Valerio Barbera, software engineer, founder, and CTO at Inspector.

Data validation is one of the fundamental features of any application and it is something developers manipulate almost every day. The value a software provides to users is often a function of the quality of data it is able to manage. Laravel ships with a lot of predefined validation rules you can immediately use in your controllers.

Anyway working on the Inspector backend we have identified some aspects of validating incoming data, which have an impact on the security and reliability of the application, and also thanks to custom rules, you can easily extend the validation layer of your app with functionalities provided by external services.

Let me start with a bit of context to clarify the role the validation layer plays in a backend service, then I’ll show you our implementations.

Laravel Validation Layer

Data integrity and validation are important aspects of web development because they define the state of the app. If data are wrong, the application behaves wrong.

It’s always important to validate data not only before storing them in the database but before doing anything.

In the Laravel request lifecycle, an HTTP request sent by a client first goes through middleware. Middleware deals with a mix of things between authentication and security.

Now, before the request enters the application, the data it carries must be validated.

Laravel Validation Layer DiagramThere are two ways to accomplish data validation in Laravel: Inside the controllers, or using Form requests.

Validation in Controller

The easiest way of validation is performing it directly in the controller. At the start of each controller method you can first validate data:

<?php

namespace AppHttpControllers;
 
use IlluminateHttpRequest;
 
class UserController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|min:3',
            'email' => 'required|email|min:6',
        ]);
 
        // here we know data are valid so we can pass them to database or other services
    }
}

Laravel will take care to return a 422 response code to the client if the data is not valid.

Use Form Requests

If your validation rules are too complex, you may want to encapsulate them in reusable classes to avoid messing up the controller.

Laravel provides the ability to wrap validation in a dedicated component called FormRequest.

First, create a form request:

php artisan make:request StoreUserRequest

Then move your validation logic inside the rules method of the request class:

<?php
 
namespace AppHttpRequests;

use IlluminateFoundationHttpFormRequest;
 
class StoreUserRequest 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 [
            'name' => 'required|string|min:3',
            'email' => 'required|email|min:6',
        ];
    }
}

You can type hint this new request class in the controller method instead of the original request class so Laravel will apply the validation rules automatically, and remove the validation statement:

<?php
 
namespace AppHttpControllers;
 
use AppHttpRequestsStoreUserRequest;
 
class UserController extends Controller
{
    public function store(StoreUserRequest $request)
    {
        // here we know data are valid so we can pass them to database or other services
    }
}

Custom Validation Rules

Laravel provided a really well-developed validation layer. It can be easily extended by implementing custom rules to be reused in your code, or to extend the capability of your validation using external services.

Let me show you a real example of one of the custom rules we implemented in Inspector.

First, create the class that represents a validation rule in Laravel:

php artisan make:rule SecurePassword

The idea is to verify if the password is in the list of well-known insecure passwords. If it is, it will not pass the validation, forcing the user to use a less common string.

<?php

namespace AppRules;

use IlluminateContractsValidationRule;

class SecurePassword implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return !in_array($value, [
            'picture1',
            'password',
            'password1',
            '12345678',
            '111111',
			
			...
        ]);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The chosen password is unsecure. Try again with a less common string.';
    }
}

Integrate With External Services

Talking about data validation there are a lot of SaaS services that can bring new capability in your validation layer in terms of security and reliability of the data collected.

I recommend you take a look at apilayer.com who provides a great set of REST services to deal with data.

In Inspector we use the mailboxlayer.com API to validate emails. The service is also able to detect fake email addresses, temporary addresses, and the actual existence of an email address using MX-Records and SMTP.

Add two configuration properties to store the API keys of the new services in the config/service.php file:

<?php

return [

    ...,
	
    'mailboxlayer' => [
        'key' => env('MAILBOXLAYER_KEY'),
    ],

    'vatlayer' => [
        'key' => env('VATLAYER_KEY'),
    ],
	
];

Create the custom rule:

php artisan make:rule EmailSpam

Here is the complete code of the rule:

<?php

namespace AppRules;

use IlluminateContractsValidationRule;

class EmailSpam implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param string $attribute
     * @param mixed $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        if (app()->environment('local')) {
            return true;
        }

        return !config('services.mailboxlayer.key') || $this->check($value);
    }

    /**
     * Perform email check.
     *
     * @param string $email
     * @return bool
     */
    protected function check(string $email): bool
    {
        try{
            $response = file_get_contents('https://apilayer.net/api/check?'.http_build_query([
                'access_key' => config('services.mailboxlayer.key'),
                'email' => '[mailbox-layer-account-email]',
                'smtp' => 1,
            ]));

            $response = json_decode($response, true);

            return $response['format_valid'] && !$response['disposable'];

        } catch (Exception $exception) {
            report($exception);

            if (app()->environment('local')) {
                return false;
            }

            // Don't block production environment in case of apilayer error
            return true;
        }
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'Invalid email address.';
    }
}

Tips and Tricks

Validate Borders

Based on my experience, I can suggest you always validate not only the minimum size of the incoming fields but also the maximum size.

Don’t wait for database errors that truncate too long strings and help your users to understand the limits of each field by the error messages returned during validation.

Ask for The Current Password

Every critical action should require password confirmation.

You should always prompt the user to type the current password to authorize actions that can compromise the account accessibility, like changing email and changing the password.

This feature will improve security because also having physical access to the computer with the Inspector dashboard opened on the screen, a malicious user can’t change access credentials without knowing the current password. He can’t shut you out.

Here is our implementation of the current password verification:

<?php

namespace AppRules;

use IlluminateContractsValidationRule;
use IlluminateSupportFacadesAuth;
use IlluminateSupportFacadesHash;

class CurrentPassword implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return Hash::check($value, Auth::user()->password);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'Your current password is incorrect.';
    }
}

.

Leave a Comment