Simple and reusable reCAPTCHA validation in Laravel

Lately I've been building an app that contains a lot of reCAPTCHA validation and I've come up with a solution for dead-simple usage for the reCAPTCHA validation and for displaying a reCAPTCHA box. Feel free to use this solution whenever you want!

Preparation

After you've registered for reCAPTCHA and obtained the keys, don't directly pass those keys to your code. Instead, add your keys to the .env file and create necessary configuration keys in services.php config file.

Note: Laravel is caching the environment variables and config once loaded, so you should never directly use env('KEY') because you'll force reloading of the environment file and we don't want that. Instead, add them to a config file and use config().

# .env
RECAPTCHA_KEY=public-key
RECAPTCHA_SECRET=secret-key
// config/services.php
'recaptcha' => [
    'key'    => env('RECAPTCHA_KEY'),
    'secret' => env('RECAPTCHA_SECRET'),
],

Now, the keys can be loaded by calling config('services.recaptcha.{key}').

Zttp

We will also use a wrapper for Guzzle called Zttp, so make sure to import it and register it: composer require kitetail/zttp. Zttp makes making requests to other endpoints a breeze, as you will see.

Validation

Since Laravel 5.3, you can create custom validation rules, so let's create one called Recaptcha. The contents of the passes($attribute, $value) will be as follows:

public function passes($attribute, $value)
{
    $response = Zttp::asFormParams()->post('https://www.google.com/recaptcha/api/siteverify', [
        'secret'   => config('services.recaptcha.secret'),
        'response' => $value,
        'remoteip' => request()->ip(),
    ]);

    return $response->json()['success'];
}

Essentially, we're POSTing to the Google's API to verify the reCAPTCHA. Secret key is loaded from the config file, client's IP is parsed by the request() helper, and the $value is value of the response key that reCAPTCHA generated, once you clicked on the I'm not a robot. Customize error message in Recaptcha@messages() to your specific needs, such as: The reCAPTCHA validation failed.

Registering the rule

Once you do that, all you need to do is append the Recaptcha class instance to the validation rules wherever you need reCAPTCHA validation and you're set. By default, Google uses the g-recaptcha-response request key to validate the field.

$rules = [
    // ... rest of the rules,
    'g-recaptcha-response' = ['required', app(Recaptcha::class)]
];

You can also register a rule by extending the validation class and use it as a string ['recaptcha']:

Validator::extend('recaptcha', 'App\Rules\Recaptcha@passes');

We're all set! Add a reCAPTCHA box to your form in view files and we got ourselves a simple reusable reCAPTCHA validation. But wait, there's more. What if you could call @captcha directive in your view file and have that I'm not a robot field right out of the box. We're gonna tackle that next.

Custom view directive

First of all, you should create a custom view file that is going to set up HTML elements and inject JS script into the code. Let's call it captcha.blade.php.

@section ('captcha-js')
    <script src="https://www.google.com/recaptcha/api.js"></script>
@endsection

<div class="g-recaptcha" data-sitekey="{{ config('services.recaptcha.key') }}"></div>

In your layout file, add @yield ('captcha-js') before closing </head> so we can add the Google's JS link.

<!-- Layout file -->
<head>
    <!-- ... -->
    @yield ('captcha-js')
</head>
<body>
<!-- ... -->

Once you create a view file, it's time to register that view file in a custom directive in your boot() method of the service provider.

Blade::directive('captcha', function () {
    return "<?php echo \$__env->make('captcha', array_except(get_defined_vars(), ['__data', '__path']))->render(); ?>";
});

This way, instead of calling @include ('captcha'), all we gotta do is call @captcha, add validation rule in the controller and we have a reCAPTCHA validation solution that is completely reusable and dead-simple to use.
We're done! If you need 5 reCAPTCHA boxes on a page, just call @captcha five times and it's all fine, the JS will only be loaded once but reCAPTCHA will be shown 5 times.

Testing

You may have noticed that by now, if you run your tests, those posting to the endpoints you now introduced reCAPTCHA to will fail. That's not a surprise. You're not passing the reCAPTCHA validation. There are many ways to bypass reCAPTCHA validation in your tests and we're gonna look at all of them, so just use the one you feel comfortable with.

Pass if running tests

Simply modify your passes() method in the Recaptcha class to pass validation if the app is in the testing environment.

if (app()->runningUnitTests()) return true;

$response = ...

This feels a bit hacky for me. We actually want to test the funcionality of the class.

Testing keys

Google has their own testing key that you can pass and it will automatically return success, that can be used in situations like this.
Modify your class to check if running in testing enviromment then use testing keys instead of real ones.

$key = app()->runningUnitTests() ? '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe' : config('services.recaptcha.secret');

$response = Zttp::asFormParams()->post('https://www.google.com/recaptcha/api/siteverify', [
    'secret'   => $key,
    'response' => $value,
    'remoteip' => request()->ip(),
]);

return $response->json()['success'];

This also feels a bit hacky because I have to modify a class for specific environments to make sure my tests pass.
These two solutions work fine, but we actually want to test our validation functionality because what if, down the road, ZTTP makes changes to their API. Our tests will pass, but in reality, it doesn't work. And on the other hand, we don't want to make a request to the Google's API every time we run our test suite since that's slow. That's where third solution comes in handy.

Mocking the class

Just mock the class whenever you need to use reCAPTCHA and you're done. I created a mockCaptcha() method in my base TestCase class that looks like this:

protected function mockCaptcha()
{
    $this->app->singleton(Recaptcha::class, function () {
        return \Mockery::mock(Recaptcha::class, function ($mock) {
            $mock->shouldReceive('passes')->andReturn(true);
        });
    });
}

Notice that if you mock the class, you cannot use new Recaptcha() in your validation rules, you will have to resolve it out of the Laravel container using app(Recaptcha::class). Now whenever testing, just call the mocking method and pass any g-recaptcha-response as your field! This way, we can actually test that reCAPTCHA has to be verified by not invoking the mock method and the Google's API can still be hit from tests.

Example:

// SomeTest.php
public function test_some_functionality()
{
    $this->mockCaptcha(); // Invoke the method to mock the reCAPTCHA

    $this->post(route('posts.store'), [
        'body' => 'Some content.', 
        'g-recaptcha-response' => 'test'
    ]);

    $this->assertCount(1, Post::all()); // Notice that reCAPTCHA validation was passed
}

public function test_recaptcha()
{
    //$this->mockCaptcha();

    $this->post(route('posts.store'), [
        'body' => 'Some content.', 
        'g-recaptcha-response' => 'test'
    ]);

    $this->assertCount(0, Post::all()); // Notice that reCAPTCHA was not verified, so our validation works
}

That's it. There is no point of importing a third-party package for such a simple functionality. This is completely maintainable and testable by yourself. If anyone has any question, feel free to contact me.

Simple and reusable reCAPTCHA validation in Laravel
Share this

Subscribe to Josip Crnković