Implement a ReCaptcha React component validated by Symfony
In this article, we are going to implement a React component to render a Google ReCaptcha V2 element and a PHP / Symfony service to check that the ReCaptcha was correctly filled by the user. There are a few subtleties concerning ReCaptcha's integration into a React component, which is why I wrote this article.
Create the ReCaptcha component
Insert first Google's script in your main HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
<script src="https://www.google.com/recaptcha/api.js"></script>
<!-- ... -->
</head>
<body>
<!-- ... -->
</body>
</html>
Create a dedicated component for rendering a ReCaptcha element:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class ReCaptcha extends Component {
constructor(props) {
super(props)
this.loadRecaptcha = this.loadRecaptcha.bind(this)
this.handleChange = this.handleChange.bind(this)
}
componentDidMount() {
if (document.readyState === 'complete') {
// Window was already loaded (the component is rendered later on)
// ReCaptacha can be safely loaded
this.loadRecaptcha()
} else {
// Wait that the window gets loaded to load the ReCaptcha
window.onload = this.loadRecaptcha
}
}
getValue() {
window.grecaptcha.getResponse(this.recatchaElt)
}
loadRecaptcha() {
const { id, apiKey, theme } = this.props
this.recatchaElt = window.grecaptcha.render(id, {
sitekey: apiKey,
theme,
callback: this.handleChange,
})
}
handleChange() {
const { onChange } = this.props
onChange(this.getValue())
}
render() {
const { id } = this.props
return <div id={id} />
}
}
ReCaptcha.propTypes = {
id: PropTypes.string.isRequired,
apiKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
theme: PropTypes.oneOf(['dark', 'light']),
}
ReCaptcha.defaultProps = {
theme: 'light',
}
export default ReCaptcha
As you can see, I defined 3 properties for the component:
id
(required): ID of the ReCaptcha element.apiKey
(required): the API key that needs to get generated on Google's website with your account.onChange
(required): a method that will handle ReCaptcha's status change in the parent component.theme
(default:light
): ReCaptcha's theme (dark
orlight
).
💡 Tip: If you are using ESLint to check code quality, you must add some configuration to avoid warnings concerning the use of the document
and window
global variables. This is my configuration in my .eslintrc.js
file at the root of my project:
module.exports = {
extends: 'airbnb',
rules: {
'react/jsx-filename-extension': 'off',
},
env: {
// 👇This line avoids warnings for browser global variables
browser: true,
},
}
You can then make use of the component in your parent component:
import React, { Component } from 'react'
import ReCaptcha from './ReCaptcha'
class App extends Component {
constructor(props) {
super(props)
this.state = {
captchaResponse: null,
}
}
render() {
return (
<div>
<ReCaptcha
id="recaptcha"
apiKey="YOUR_API_KEY"
onChange={(response) => {
this.setState({ captchaResponse: response })
}}
/>
</div>
)
}
}
export default App
Backend check using PHP / Symfony
Let's create a service that will check the ReCaptcha value (captchaResponse
in the state of the example component making use of the ReCaptcha
component above).
This service is designed for Symfony, but the cool thing is that Symfony's services are pure PHP objects and are therefore framework agnostic, so these classes may be used in any PHP project.
Captcha checker interface
Let's define first a Captcha checker interface, so that the switch to any other Captcha system in the future could be easily made:
<?php
namespace App\Captcha;
use GuzzleHttp\Client;
interface CaptchaCheckerInterface
{
/**
* Checks captcha response
*
* @param string $captchaResponse
* @return bool
*/
public function check(string $captchaResponse): bool;
}
Captcha checker for ReCaptcha
Install Guzzle first:
composer require guzzlehttp/guzzle:~6.0
Define the ReCaptcha checker:
<?php
namespace App\Captcha;
use GuzzleHttp\Client;
class GoogleReCaptchaChecker implements CaptchaCheckerInterface
{
protected $secret;
public function __construct(string $secret)
{
$this->secret = $secret;
}
/**
* {@inheritDoc}
*/
public function check(string $captchaValue): bool
{
$response = $this->getCaptchaResponse($captchaValue);
// Better checks could be done here
if ($data && isset($data['success']) && true === $data['success']) {
return true;
}
return false;
}
private function getCaptchaResponse($captchaValue): array
{
$response = $this->getClient()->request(
'POST',
'recaptcha/api/siteverify',
[
'form_params' => [
'secret' => $this->secret,
'response' => $captchaValue,
],
]
);
return json_decode($response->getBody(), true);
}
private function getClient(): Client
{
return new Client([
'base_uri' => 'https://www.google.com',
]);
}
}
If you're using Symfony, define your ReCaptcha secret provided by Google as an environment variable and autoconfigure the corresponding service:
# config/services.yaml
services:
App\Service\CaptchaChecker:
arguments:
$secret: '%env(GOOGLE_RECAPTCHA_SECRET)%'
Make use of the service to check Captcha response value
You can now make use of the service anywhere, like in a controller:
<?php
namespace App\Controller;
use App\Captcha\CaptchaCheckerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class CaptchaController extends Controller
{
/**
* @Route("/captcha/check")
*/
public function check(Request $request, CaptchaCheckerInterface $captchaChecker)
{
$isCaptchaValid = $captchaChecker->check($request->request->get('captcha'));
// ...
}
}