reCAPTCHA v3 returns a score for each request without user friction. The score is based on interactions with your site (1.0 is very likely a good interaction, 0.0 is very likely a bot) and enables you to take an appropriate action for your site. Register reCAPTCHA v3 keys here.
With composer, require:
composer require karser/karser-recaptcha3-bundle
You can quickly configure this bundle by using symfony/flex:
Symfony 4/5 Version :
Register bundle into config/bundles.php
:
return [
//...
Karser\Recaptcha3Bundle\KarserRecaptcha3Bundle::class => ['all' => true],
];
Symfony 3 Version:
Register bundle into app/AppKernel.php
:
public function registerBundles()
{
return array(
// ...
new Karser\Recaptcha3Bundle\KarserRecaptcha3Bundle(),
);
}
# config/packages/karser_recaptcha3.yaml (or app/config/config.yml if using Symfony3)
karser_recaptcha3:
site_key: '%env(RECAPTCHA3_KEY)%'
secret_key: '%env(RECAPTCHA3_SECRET)%'
score_threshold: 0.5
Add your site key and secret to your .env file:
###> karser/recaptcha3-bundle ###
RECAPTCHA3_KEY=my_site_key
RECAPTCHA3_SECRET=my_secret
###< karser/recaptcha3-bundle ###
<?php
use Karser\Recaptcha3Bundle\Form\Recaptcha3Type;
use Karser\Recaptcha3Bundle\Validator\Constraints\Recaptcha3;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('captcha', Recaptcha3Type::class, [
'constraints' => new Recaptcha3(),
'action_name' => 'homepage',
'script_nonce_csp' => $nonceCSP,
]);
}
}
Notes:
- The
action_name
parameter is reCAPTCHA v3 action which identifies the submission of this particular form in the Google reCAPTCHA dashboard, and confirming it is as expected in the backend is a recommended extra security step. - The
script_nonce_csp
parameter is optional. You must use the same nonce as in your Content-Security Policy header.
The idea is to require the frontend to submit the captcha token, so it will be validated on server side.
First you need to add the captcha field to your transport entity:
<?php
namespace App\Dto;
final class UserSignupRequest
{
/** @var string|null */
public $email;
/** @var string|null */
public $captcha;
}
And to add the validation constraint:
#config/validator/validation.yaml
App\Dto\UserSignupRequest:
properties:
email:
- NotBlank: ~
- Email: { mode: strict }
captcha:
- Karser\Recaptcha3Bundle\Validator\Constraints\Recaptcha3: ~
On frontend part you need to submit the captcha token along with email. You can obtain the captcha token either on page load or on form submit.
<script src="https://www.google.com/recaptcha/api.js?render=<siteKey>"></script>
<script>
const siteKey = '*****************-**-******-******';
//either on page load
grecaptcha.ready(function() {
grecaptcha.execute(siteKey, {
action: 'homepage'
}).then(function(token) {
//the token will be sent on form submit
$('[name="captcha"]').val(token);
//keep in mind that token expires in 120 seconds so it's better to add setTimeout.
});
});
//or on form post:
grecaptcha.ready(function() {
grecaptcha.execute(siteKey, {
action: 'homepage'
}).then(function(token) {
//submit the form
return http.post(url, {email, captcha: token});
});
});
</script>
Just add the {{ errorCodes }}
variable to the message template:
$formBuilder->add('captcha', Recaptcha3Type::class, [
'constraints' => new Recaptcha3(['message' => 'There were problems with your captcha. Please try again or contact with support and provide following code(s): {{ errorCodes }}']),
])
Recaptcha won't allow you to test your app efficiently unless you disable it for the environment you are testing against.
# app/config/config.yml (or config/packages/karser_recaptcha3.yaml if using Symfony4)
karser_recaptcha3:
enabled: '%env(bool:RECAPTCHA3_ENABLED)%'
#.env.test or an environment variable
RECAPTCHA3_ENABLED=0
You should inject @karser_recaptcha3.google.recaptcha
in your service and call setScoreThreshold
method.
#services.yaml
App\Services\YourService:
arguments: ['@karser_recaptcha3.google.recaptcha']
#App/Services/YourService.php
use ReCaptcha\ReCaptcha;
class YourService {
private $reCaptcha;
public function __construct(ReCaptcha $reCaptcha) {
$this->reCaptcha = $reCaptcha;
}
public function yourMethod() {
$this->reCaptcha->setScoreThreshold(0.7);
}
}
From the Cloudflare docs: To provide the client (visitor) IP address for every request to the origin, Cloudflare adds the CF-Connecting-IP header.
"CF-Connecting-IP: A.B.C.D"
So you can implement custom IP resolver which attempts to read the CF-Connecting-IP
header or fallbacks with the internal IP resolver:
<?php declare(strict_types=1);
namespace App\Service;
use Karser\Recaptcha3Bundle\Services\IpResolverInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class CloudflareIpResolver implements IpResolverInterface
{
/** @var IpResolverInterface */
private $decorated;
/** @var RequestStack */
private $requestStack;
public function __construct(IpResolverInterface $decorated, RequestStack $requestStack)
{
$this->decorated = $decorated;
$this->requestStack = $requestStack;
}
public function resolveIp(): ?string
{
return $this->doResolveIp() ?? $this->decorated->resolveIp();
}
private function doResolveIp(): ?string
{
$request = $this->requestStack->getCurrentRequest();
if ($request === null) {
return null;
}
return $request->server->get('HTTP_CF_CONNECTING_IP');
}
}
Here is the service declaration. It decorates the internal resolver:
#services.yaml
services:
App\Service\CloudflareIpResolver:
decorates: 'karser_recaptcha3.ip_resolver'
arguments:
$decorated: '@App\Service\CloudflareIpResolver.inner'
$requestStack: '@request_stack'
Also, make sure you added the domain you use in the recaptcha settings. Usually dev domain differs from the production one, so better to double check.
<input type="hidden" id="form_captcha" name="form[captcha]" /><script>
var recaptchaCallback_form_captcha = function() {
grecaptcha.execute('<YOUR-RECAPTCHA-KEY>', {action: 'landing'}).then(function(token) {
document.getElementById('form_captcha').value = token;
});
};
</script><script src="https://www.google.com/recaptcha/api.js?render=<YOUR-RECAPTCHA-KEY>&onload=recaptchaCallback_form_captcha" async defer></script>
</form>
composer update
vendor/bin/phpunit