Skip to content

Commit

Permalink
BREAKING: remove action=form default from ReCaptcha V3 widget
Browse files Browse the repository at this point in the history
Don't pass a default action to the ReCaptcha API anymore. Specifying an
action is an optional feature of the ReCaptcha API and thus we should
make it optional too by default.
  • Loading branch information
Stormheg committed Nov 14, 2023
1 parent f5ff004 commit 8048460
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 11 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,11 @@ validation will fail as expected and an error message will be logged.

### reCAPTCHA V3 Action

The V3 reCAPTCHA supports an
[action](https://developers.google.com/recaptcha/docs/v3#actions) value
that provides break-downs of actions and adaptive risk analysis.
[Google's reCAPTCHA V3 API supports passing an action value](https://developers.google.com/recaptcha/docs/v3#actions).
Actions allow you to tie reCAPTCHA validations to a specific form on your site for analytical purposes, enabling you to perform risk analysis per form. This will allow you to make informed decisions about adjusting the score threshold for certain forms because abusive behavior can vary depending on the nature of the form.

To set the action value, pass it when instantiating the ReCaptcha
widget. By default it is set to <span class="title-ref">form</span>.
To set the action value, pass an `action` argument when instantiating the ReCaptcha
widget. Be careful to only use alphanumeric characters, slashes and underscores as stated in the reCAPTCHA documentation.

```python
captcha = fields.ReCaptchaField(
Expand All @@ -252,6 +251,8 @@ captcha = fields.ReCaptchaField(
)
```

Setting an action is entirely optional. If you don't specify an action, no action will be passed to the reCAPTCHA V3 API.

### Local Development and Functional Testing

If `RECAPTCHA_PUBLIC_KEY` and `RECAPTCHA_PRIVATE_KEY` are not set,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
});
function recaptchaFormSubmit(event) {
event.preventDefault();
{% if action %}
grecaptcha.execute('{{ public_key }}', {action: '{{ action|escape }}'})
{% else %}
grecaptcha.execute('{{ public_key }}', {})
{% endif %}
.then(function(token) {
console.log("reCAPTCHA validated for 'data-widget-uuid=\"{{ widget_uuid }}\"'. Setting input value...")
element.value = token;
Expand Down
54 changes: 49 additions & 5 deletions django_recaptcha/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,38 @@ class InvisForm(forms.Form):
self.assertIn('data-widget-uuid="%s"' % test_hex, html)
self.assertIn('data-sitekey="pubkey"', html)
self.assertIn('.g-recaptcha[data-widget-uuid="%s"]' % test_hex, html)
# By default, the action should NOT be in the JS code
self.assertNotIn("action", html)

@patch("django_recaptcha.widgets.uuid.UUID.hex", new_callable=PropertyMock)
def test_default_v3_html_with_action(self, mocked_uuid):
test_hex = "c7a86421ca394661acccea374931d260"
mocked_uuid.return_value = test_hex

class InvisForm(forms.Form):
# We want to check that the action is passed to the JS code
captcha = fields.ReCaptchaField(widget=widgets.ReCaptchaV3(action="needle"))

form = InvisForm()
html = form.as_p()
self.assertIn(
'<script src="https://www.google.com/recaptcha/api.js'
'?render=pubkey"></script>',
html,
)
# ReCaptcha V3 widget has input_type=hidden, there should be no label element in the html
self.assertNotIn("label", html)

self.assertIn('data-size="normal"', html)
self.assertIn('data-callback="onSubmit_%s"' % test_hex, html)
self.assertIn('class="g-recaptcha"', html)
self.assertIn("required", html)
self.assertIn('data-widget-uuid="%s"' % test_hex, html)
self.assertIn('data-sitekey="pubkey"', html)
self.assertIn('.g-recaptcha[data-widget-uuid="%s"]' % test_hex, html)

# Expect the action to be in the JS code
self.assertIn("action: 'needle'", html)

@patch("django_recaptcha.widgets.uuid.UUID.hex", new_callable=PropertyMock)
def test_v3_attribute_changes_html(self, mocked_uuid):
Expand Down Expand Up @@ -302,7 +334,7 @@ class VThreeDomainForm(forms.Form):
)

mocked_submit.return_value = RecaptchaResponse(
is_valid=True, extra_data={"score": 0.9}, action="form"
is_valid=True, extra_data={"score": 0.9}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
Expand All @@ -328,7 +360,7 @@ class VThreeDomainForm(forms.Form):
captcha = fields.ReCaptchaField(widget=widgets.ReCaptchaV3())

mocked_submit.return_value = RecaptchaResponse(
is_valid=True, extra_data={"score": 0.1}, action="form"
is_valid=True, extra_data={"score": 0.1}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
Expand All @@ -337,23 +369,35 @@ class VThreeDomainForm(forms.Form):
@patch("django_recaptcha.fields.client.submit")
def test_client_invalid_action_v3(self, mocked_submit):
class VThreeDomainForm(forms.Form):
captcha = fields.ReCaptchaField(widget=widgets.ReCaptchaV3())
captcha = fields.ReCaptchaField(widget=widgets.ReCaptchaV3(action="form"))

mocked_submit.return_value = RecaptchaResponse(
is_valid=True, extra_data={"score": 0.1}, action="not_form"
is_valid=True, extra_data={"score": 0.1}, action="something_unexpected"
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertFalse(form.is_valid())

@patch("django_recaptcha.fields.client.submit")
def test_client_valid_action_v3(self, mocked_submit):
class VThreeDomainForm(forms.Form):
captcha = fields.ReCaptchaField(widget=widgets.ReCaptchaV3(action="form"))

mocked_submit.return_value = RecaptchaResponse(
is_valid=True, extra_data={"score": 0.1}, action="form"
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertTrue(form.is_valid())

@patch("django_recaptcha.fields.client.submit")
@override_settings(RECAPTCHA_REQUIRED_SCORE=0.0)
def test_required_score_human_setting(self, mocked_submit):
class VThreeDomainForm(forms.Form):
captcha = fields.ReCaptchaField(widget=widgets.ReCaptchaV3())

mocked_submit.return_value = RecaptchaResponse(
is_valid=True, extra_data={"score": 0.85}, action="form"
is_valid=True, extra_data={"score": 0.85}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
Expand Down
2 changes: 1 addition & 1 deletion django_recaptcha/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ReCaptchaV3(ReCaptchaBase):
input_type = "hidden"
template_name = "django_recaptcha/widget_v3.html"

def __init__(self, api_params=None, action="form", *args, **kwargs):
def __init__(self, api_params=None, action=None, *args, **kwargs):
super().__init__(api_params=api_params, *args, **kwargs)
if not self.attrs.get("required_score", None):
self.attrs["required_score"] = getattr(
Expand Down

0 comments on commit 8048460

Please sign in to comment.