Implementing Google reCaptcha in a Magento custom form

As a reaction to the issue of malicious targeting on PayPal Payflow Pro integration, Magento, from version 2.2.9 and 2.3.2, introduced a new module to the basic packages: magento/module-paypal-recaptcha (more info here: PayPal Payflow Pro active carding activity – Magento Help Center).

The PaypalReCaptcha module is based on msp/recaptcha (msp/recaptcha – Packagist), which has been included in Magento Security Extensions (GitHub – magento/security-package: Magento Security Extensions) and is no longer supported. There is no official magento/security-package available at the time of writing this article.

Our example is based on the msp/recaptcha module. This module provides Google reCaptcha integrations in forms like: Customer Login, Forgot password, Create customer or Newsletter, and others. Google reCaptcha can be implemented easily in a custom form – it’s available in the latest Magento version or you can install the msp/recaptcha module.

To find the answer how to configure Google reCaptcha, you can visit: Google reCAPTCHA | Magento Commerce 2.3 User Guide.

We need a Google API website key and a Google API secret key, which has to be filled in Stores -> Configuration -> Security -> Google reCaptcha.

Magento admin panel

Let’s move on to the main topic, which is integration with a custom form.

First, we have to create the module. Let’s name it Magently_CustomReCaptcha:

file: app/code/Magently/CustomReCaptcha/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magently_CustomReCaptcha',
    __DIR__
);
file: app/code/Magently/CustomReCaptcha/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Magently_CustomReCaptcha" setup_version="1.0.0">
        <sequence>
            <module name="MSP_ReCaptcha"/>
        </sequence>
    </module>
</config>

The next step is to create a simple form. We need a layout, a template, and two controllers – first for displaying and second for saving. The name of our module suggests providing reCaptcha to a custom form, so we would normally need separate modules, but let’s make things simple for the sake of the examle:

Controllers:

file: app/code/Magently/CustomReCaptcha/etc/frontend/routes.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="magently_form" frontName="magently_form">
            <module name="Magently_CustomReCaptcha" />
        </route>
    </router>
</config>
file: app/code/Magently/CustomReCaptcha/Controller/Index/View.php

<?php

namespace Magently\CustomReCaptcha\Controller\Index;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;

/**
 * Class View
 */
class View extends Action
{
    /**
     * @var PageFactory
     */
    private $pageFactory;

    /**
     * @param Context $context
     * @param PageFactory $pageFactory
     */
    public function __construct(Context $context, PageFactory $pageFactory)
    {
        parent::__construct($context);
        $this->pageFactory = $pageFactory;
    }

    /**
     * @return \Magento\Framework\View\Result\Page
     */
    public function execute()
    {
        return $this->pageFactory->create();
    }
}
file: app/code/Magently/CustomReCaptcha/Controller/Index/Save.php

<?php

namespace Magently\CustomReCaptcha\Controller\Index;

use Magento\Framework\App\Action\Action;

/**
 * Class Save
 */
class Save extends Action
{
    /**
     * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void
     */
    public function execute()
    {
        // custom logic to save the form
        $this->_redirect('magently_form/index/view');

    }
}

View:

file: app/code/Magently/CustomReCaptcha/view/frontend/layout/magently_form_index_view.xml (part1)

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block name="magently_form" template="Magently_CustomReCaptcha::form.phtml">
                <container name="form.additional.info" />
            </block>
        </referenceContainer>
    </body>
</page>
file: app/code/Magently/CustomReCaptcha/view/frontend/templates/form.phtml

<?php
/** @var \Magento\Framework\View\Element\Template $block */
?>

<form id="magently_form" method="post" action="<?= $block->getUrl('magently_form/index/save') ?>">
    <label for="input1">Input 1</label>
    <input id="input1" name="input1" type="text" />

    <label for="input1">Input 2</label>
    <input id="input2" name="input2" type="text" />

    <?= $block->getChildHtml('form.additional.info') ?>

    <input type="submit" value="<?= $block->escapeHtml(__('Send')) ?>">
</form>

The form is displayed:

and the save feature is functional.

Magento panel

Implementing Google ReCaptcha

Now that we’re done with the preparation, let’s move on to the main task at hand – implementing Google reCaptcha in the custom form we just created.

The MSP_ReCaptcha module is easily extendable. Thanks to Dependency Injection, we won’t need to write a lot of code (for further reading, check my other articles Virtual Types, Types, Preferences: Magento 2 Design Patterns pt. 2 and Magento 2 Design Patterns: ViewModel and Proxy). We’re going to need: 

  • a definition in system.xml with our field that will enable/disable reCaptcha
  • classes for retrieving the configuration
  • a plugin which will return our option in “zones” – places where reCaptcha is switched on
  • a configuration in layout.xml to embed the block rendering reCaptcha
  • to define an observer, which will run before executing the controller 
  • to define configuration in di.xml

ReCaptcha configuration can be displayed in a different place than its default location, but in this example we’ll just use the default subpage Stores -> Configuration -> Security -> Google reCaptcha -> Frontend:

file: app/code/Magently/CustomReCaptcha/etc/adminhtml/system.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="msp_securitysuite_recaptcha">
            <group id="frontend">
                <field id="enabled_custom_form"
                       translate="label"
                       type="select"
                       sortOrder="300"
                       showInDefault="1"
                       showInWebsite="1"
                       showInStore="0"
                       canRestore="1">
                    <label>Use in Magently Custom Form</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                    <depends>
                        <field id="enabled">1</field>
                    </depends>
                </field>
            </group>
        </section>
    </system>
</config>

We insert our option and it should be visible as on the screen

The next step we need to take is to create a class which will return the value of this field. Don’t forget to check if area Frontend is switched on. We can use the original class responsible for returning the reCaptcha configuration:

file app/code/Magently/CustomReCaptcha/Model/Config.php

<?php

namespace Magently\CustomReCaptcha\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use MSP\ReCaptcha\Model\Config as ReCaptchaConfig;

/**
 * Class Config
 * The class responsible for providing configuration
 */
class Config
{
    const XML_PATH_ENABLED_FRONTEND_CUSTOM_FORM = 'msp_securitysuite_recaptcha/frontend/enabled_custom_form';

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var ReCaptchaConfig
     */
    private $recaptchaConfig;

    /**
     * Config constructor.
     * @param ScopeConfigInterface $scopeConfig
     * @param ReCaptchaConfig $recaptchaConfig
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        ReCaptchaConfig $recaptchaConfig
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->recaptchaConfig = $recaptchaConfig;
    }

    /**
     * Return true if enabled on frontend Magently custom form page
     * @return boolean
     */
    public function isEnabledFrontendCustomForm()
    {
        if (!$this->recaptchaConfig->isEnabledFrontend()) {
            return false;
        }

        return (bool) $this->scopeConfig->getValue(
            self::XML_PATH_ENABLED_FRONTEND_CUSTOM_FORM,
            ScopeInterface::SCOPE_WEBSITE
        );
    }
}

Now, we need to modify the array, which is passed to reCaptcha components. We have to do it in order to determine whether should reCaptcha display in this form or not. It has to be defined in our plugin in di.xml:

file: app/code/Magently/CustomReCaptcha/etc/frontend/di.xml (part1)

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="MSP\ReCaptcha\Model\LayoutSettings">
        <plugin name="magently_custom_form_recaptcha_zone"
                type="Magently\CustomReCaptcha\Plugin\ReCaptcha\Model\LayoutSettings"
                sortOrder="10" />
    </type>
</config>

We also need to define the class of the plugin. Create the after plugin for the getCaptchaSettings() method of the original class and add your config, which will determine whether captcha is switched on or off in our form:

file: app/code/Magently/CustomReCaptcha/Plugin/ReCaptcha/Model/LayoutSettings.php

<?php

namespace Magently\CustomReCaptcha\Plugin\ReCaptcha\Model;

use MSP\ReCaptcha\Model\LayoutSettings as Subject;
use Magently\CustomReCaptcha\Model\Config;

/**
 * Class LayoutSettings
 * The class responsible for adding custom_form zone to MSP_ReCaptcha Layout setting
 */
class LayoutSettings
{
    /**
     * @var Config
     */
    private $config;

    /**
     * LayoutSettings constructor.
     * @param Config $config
     */
    public function __construct(Config $config)
    {
        $this->config = $config;
    }

    /**
     * @param Subject $subject
     * @param array $result
     * @return array
     */
    public function afterGetCaptchaSettings(Subject $subject, array $result)
    {
        if (isset($result['enabled'])) {
            $result['enabled']['custom_form'] = $this->config->isEnabledFrontendCustomForm();
        }
        return $result;
    }
}

The next step is to insert reCaptcha block to our form. We need to extend our file with an additional block:

app/code/Magently/CustomReCaptcha/view/frontend/layout/magently_form_index_view.xml (part2)

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block name="magently_form" template="Magently_CustomReCaptcha::form.phtml">
                <container name="form.additional.info">
                    <block class="MSP\ReCaptcha\Block\Frontend\ReCaptcha" name="msp-recaptcha" after="-"
                           template="MSP_ReCaptcha::msp_recaptcha.phtml"
                           cacheable="false"
                           ifconfig="msp_securitysuite_recaptcha/frontend/enabled">
                        <arguments>
                            <argument name="jsLayout" xsi:type="array">
                                <item name="components" xsi:type="array">
                                    <item name="msp-recaptcha" xsi:type="array">
                                        <item name="component" xsi:type="string">MSP_ReCaptcha/js/reCaptcha</item>
                                        <item name="zone" xsi:type="string">custom_form</item>
                                    </item>
                                </item>
                            </argument>
                        </arguments>
                    </block>
                </container>
            </block>
        </referenceContainer>
    </body>
</page>

In the block arguments, we have to pass configuration of the block with the specified name of the “zone” to the jsLayout array. 

The last and most important step is to create an observer for our controller. Its task is to check if reCaptcha verification was successful. Without this step, reCaptcha will be displayed but won’t have any functionality. So let’s get on with it!

file: app/code/Magently/CustomReCaptcha/etc/frontend/di.xml (part2)

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="MSP\ReCaptcha\Model\LayoutSettings">
        <plugin name="magently_custom_form_recaptcha_zone"
                type="Magently\CustomReCaptcha\Plugin\ReCaptcha\Model\LayoutSettings"
                sortOrder="10" />
    </type>
    <!-- Magently Custom Form -->
    <virtualType name="Magently\CustomReCaptcha\Model\Provider\IsCheckRequired\Frontend\CustomForm"
                 type="MSP\ReCaptcha\Model\IsCheckRequired">
        <arguments>
            <argument name="enableConfigFlag"
                      xsi:type="string">msp_securitysuite_recaptcha/frontend/enabled_custom_form</argument>
            <argument name="area" xsi:type="string">frontend</argument>
<!--            <argument name="requireRequestParam" xsi:type="string">custom_param</argument>-->
        </arguments>
    </virtualType>
    <virtualType name="Magently\CustomReCaptcha\Model\Provider\Failure\CustomFormObserver"
                 type="MSP\ReCaptcha\Model\Provider\Failure\ObserverRedirectFailure">
        <arguments>
            <argument name="redirectUrlProvider"
                      xsi:type="object">MSP\ReCaptcha\Model\Provider\Failure\RedirectUrl\ReferrerUrlProvider</argument>
        </arguments>
    </virtualType>
    <virtualType name="Magently\CustomReCaptcha\Observer\Frontend\CustomFormObserver"
                 type="MSP\ReCaptcha\Observer\ReCaptchaObserver">
        <arguments>
            <argument name="isCheckRequired"
                      xsi:type="object">Magently\CustomReCaptcha\Model\Provider\IsCheckRequired\Frontend\CustomForm</argument>
            <argument name="responseProvider"
                      xsi:type="object">MSP\ReCaptcha\Model\Provider\Response\DefaultResponseProvider</argument>
            <argument name="failureProvider"
                      xsi:type="object">Magently\CustomReCaptcha\Model\Provider\Failure\CustomFormObserver</argument>
        </arguments>
    </virtualType>
</config>

We have just created three virutalTypes. 

The first: Magently\CustomReCaptcha\Model\Provider\IsCheckRequired\Frontend\CustomForm is responsible for validation if using reCaptcha is relevant. The class that is the base of our virtualType checks three things:

  • if an area is switched on (in our case the frontend)
  • if a “zone” is switched on – the switcher Yes/No, which we added in the configuration
  • if the request is correct – eg. we can add another parameter to the form which will determine if reCaptcha should be checked

The next virtualType is a provider of the action which should run in case of incorrect verification. In our case, it’s the default Failure Provider from the module, which returns to the form and shows an error message about an incorrect verification.

The last of the virtualTypes we created is an observer, which will be used in a moment in the events.xml file. We inject our virtualTypes here.

Certainly, we can also write our own implementations, eg. for  IsCheckRequired – it only needs an implementation of a correct interface.

Now, we have to connect our observer to the predispatch action of the controller, which is responsible for saving of the form:

file: app/code/Magently/CustomReCaptcha/etc/frontend/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="controller_action_predispatch_magently_form_index_save">
        <observer name="magently_custom_form_recaptcha" instance="Magently\CustomReCaptcha\Observer\Frontend\CustomFormObserver" />
    </event>
</config>

And that’s it. Remember about cache and code compilation. The reCaptcha is implemented and can be switched on or off on demand.

Magento panel

This article also shows how powerful virtualTypes and Observers are. You can have reCaptcha validation in your form without creating any custom logic.