Skip to content

Commit

Permalink
Merge branch 'release/2.0.5'
Browse files Browse the repository at this point in the history
  • Loading branch information
AltusBarry committed Aug 26, 2019
2 parents a33a443 + b1b922a commit da958b6
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
=========

2.0.5
-----
#. Added settings and kwargs that allow for the validation of reCAPTCHA v3 score values.

2.0.4
-----
#. Fixed travis tests for django 2.2
Expand Down
28 changes: 28 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,34 @@ The templates are located in:

For more information about overriding templates look at `Django's template override <https://docs.djangoproject.com/en/2.1/howto/overriding-templates/>`_

reCAPTCHA v3 Score
~~~~~~~~~~~~~~~~~~

As of version 3, reCAPTCHA also returns a score value. This can be used to determine the likelihood of the page interaction being a bot. See the Google `documentation <https://developers.google.com/recaptcha/docs/v3#score>`_ for more details.

To set a project wide score limit use the ``RECAPTCHA_REQUIRED_SCORE`` setting.

For example:

.. code-block:: python
RECAPTCHA_REQUIRED_SCORE = 0.85
For per field, runtime, specification the attribute can also be passed to the widget:

.. code-block:: python
captcha = fields.ReCaptchaField(
widget=ReCaptchaV3(
attrs={
'required_score':0.85,
...
}
)
)
In the event the score does not meet the requirements, the field validation will fail as expected and an error message will be logged.

Local Development and Functional Testing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
8 changes: 5 additions & 3 deletions captcha/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@


class RecaptchaResponse(object):
def __init__(self, is_valid, error_codes=None):
def __init__(self, is_valid, error_codes=None, extra_data=None):
self.is_valid = is_valid
self.error_codes = error_codes or []
self.extra_data = extra_data or {}


def recaptcha_request(params):
Expand Down Expand Up @@ -66,6 +67,7 @@ def submit(recaptcha_response, private_key, remoteip):
data = json.loads(response.read().decode("utf-8"))
response.close()
return RecaptchaResponse(
is_valid=data["success"],
error_codes=data.get("error-codes")
is_valid=data.pop("success"),
error_codes=data.pop("error-codes", None),
extra_data=data
)
27 changes: 26 additions & 1 deletion captcha/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from captcha import client
from captcha._compat import HTTPError, urlencode
from captcha.constants import TEST_PRIVATE_KEY, TEST_PUBLIC_KEY
from captcha.widgets import ReCaptchaV2Checkbox, ReCaptchaBase
from captcha.widgets import ReCaptchaV2Checkbox, ReCaptchaBase, ReCaptchaV3


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -91,3 +91,28 @@ def validate(self, value):
self.error_messages["captcha_invalid"],
code="captcha_invalid"
)

required_score = self.widget.attrs.get("required_score")
if required_score:
# Our score values need to be floats, as that is the expected
# response from the Google endpoint. Rather than ensure that on
# the widget, we do it on the field to better support user
# subclassing of the widgets.
required_score = float(required_score)

# If a score was expected but non was returned, default to a 0,
# which is the lowest score that it can return. This is to do our
# best to assure a failure here, we can not assume that a form
# that needed the threshold should be valid if we didn't get a
# value back.
score = float(check_captcha.extra_data.get("score", 0))

if required_score > score:
logger.error(
"ReCAPTCHA validation failed due to its score of %s"
" being lower than the required amount." % score
)
raise ValidationError(
self.error_messages["captcha_invalid"],
code="captcha_invalid"
)
71 changes: 71 additions & 0 deletions captcha/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,74 @@ class VThreeDomainForm(forms.Form):
'?render=pubkey"></script>',
html
)

@patch("captcha.fields.client.submit")
def test_client_success_response_v3(self, mocked_submit):
class VThreeDomainForm(forms.Form):
captcha = fields.ReCaptchaField(
widget=widgets.ReCaptchaV3(attrs={'required_score': 0.8})
)
mocked_submit.return_value = RecaptchaResponse(
is_valid=True,
extra_data={'score': 0.9}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertTrue(form.is_valid())

@patch("captcha.fields.client.submit")
def test_client_failure_response_v3(self, mocked_submit):
class VThreeDomainForm(forms.Form):
captcha = fields.ReCaptchaField(
widget=widgets.ReCaptchaV3(attrs={'required_score': 0.8})
)
mocked_submit.return_value = RecaptchaResponse(
is_valid=True,
extra_data={'score': 0.1}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertFalse(form.is_valid())

@patch("captcha.fields.client.submit")
def test_client_empty_score_threshold_v3(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.1}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertTrue(form.is_valid())

@patch("captcha.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}
)
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertTrue(form.is_valid())

@patch("captcha.fields.client.submit")
@override_settings(RECAPTCHA_REQUIRED_SCORE=0.85)
def test_required_score_bot_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})
form_params = {"captcha": "PASSED"}
form = VThreeDomainForm(form_params)
self.assertFalse(form.is_valid())
9 changes: 9 additions & 0 deletions captcha/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ def build_attrs(self, base_attrs, extra_attrs=None):
class ReCaptchaV3(ReCaptchaBase):
template_name = "captcha/widget_v3.html"

def __init__(self, api_params=None, *args, **kwargs):
super(ReCaptchaV3, self).__init__(
api_params=api_params, *args, **kwargs
)
if not self.attrs.get("required_score", None):
self.attrs["required_score"] = getattr(
settings, "RECAPTCHA_REQUIRED_SCORE", None
)

def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super(ReCaptchaV3, self).build_attrs(
base_attrs, extra_attrs
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='django-recaptcha',
version='2.0.4',
version='2.0.5',
description='Django recaptcha form field/widget app.',
long_description=long_desc,
author='Praekelt Consulting',
Expand Down

0 comments on commit da958b6

Please sign in to comment.