-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Google reCaptcha Support #61
Comments
Hi @joshvillbrandt, |
Great! Our implementation definitely won't be able to be merged directly as our current implementation takes place outside of rest registration using the custom serializer settings when available. We are doing final testing of this initial implementation today. We'll share the results on here today or tomorrow and then we use this to guide an internal implementation. More soon. Thanks! |
Great news! @coffeegoddd and I successfully completed our reCaptcha integration this week. Here are some of the highlights. The general approach we took is to use the serializer settings. This allowed us to protect the RECAPTCHA_SECRET = os.environ.get('RECAPTCHA_SECRET', 'itsasecret!')
RECAPTCHA_MIN_SCORE = float(os.environ.get('RECAPTCHA_MIN_SCORE', '0.5'))
REST_REGISTRATION = {
# recaptcha intercept serializers
'LOGIN_SERIALIZER_CLASS': 'app.registration.LoginSerializer',
'REGISTER_SERIALIZER_CLASS': 'app.registration.RegisterSerializer',
'SEND_RESET_PASSWORD_LINK_SERIALIZER_CLASS': 'app.registration.ResetPasswordSerializer',
} These serializers expect an extra field ( Those serializers looks like this: from rest_framework.serializers import ValidationError, Serializer, CharField
from rest_registration.api.serializers import DefaultRegisterUserSerializer, DefaultLoginSerializer, DefaultSendResetPasswordLinkSerializer
from app.settings import RECAPTCHA_SECRET, RECAPTCHA_MIN_SCORE
import requests
import logging
# Get an instance of a logger
logger = logging.getLogger(__name__)
class RecaptchaSerializerMixin(Serializer):
# support recaptcha v3
recaptchaToken = CharField()
def validate_recaptchaToken(self, recaptchaToken):
recaptchaUrl = 'https://www.google.com/recaptcha/api/siteverify'
response = requests.post(recaptchaUrl, data={
'secret': RECAPTCHA_SECRET,
'response': recaptchaToken,
})
data = response.json()
# get local action
local_action = getattr(self, '_action', None)
# get remote action
remote_action = data.get('action', None)
# check score, success, and both actions
if local_action is None or remote_action is None \
or local_action != remote_action \
or data.get('score', 0) < RECAPTCHA_MIN_SCORE \
or data.get('success', False) is False:
logger.error({'recaptchaToken-errors': data.get('error-codes', 'Unable to verify token')})
raise ValidationError('Unable to verify token.')
return recaptchaToken
# custom serializer for account registration
class RegisterSerializer(RecaptchaSerializerMixin, DefaultRegisterUserSerializer):
# set action
_action = 'register'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.Meta.fields = self.Meta.fields + ('recaptchaToken',)
def create(self, validated_data):
if validated_data.get('recaptchaToken', None) is not None:
del validated_data['recaptchaToken']
return super().create(validated_data)
# custom serializer for login
class LoginSerializer(RecaptchaSerializerMixin, DefaultLoginSerializer):
_action = 'login'
# custom serializer for reset password
class ResetPasswordSerializer(RecaptchaSerializerMixin, DefaultSendResetPasswordLinkSerializer):
_action = 'send_reset_password_link' We also have a few unit tests which I won't copy here. I will show off our shiny reCaptcha mock class though! import json
from copy import deepcopy
class MockRecaptcha():
def __init__(self, mocker):
self.requests = []
self._next_response = None
mocker.post('https://www.google.com/recaptcha/api/siteverify', text=self._siteverify_post)
self.configure_next_response()
def configure_next_response(self, success=True, score=0.9, action='test'):
self._next_response = {
'success': success,
'challenge_ts': '2019-06-25T16:21:55Z',
'hostname': 'localhost',
'score': score,
'action': action,
}
def _siteverify_post(self, request, context):
# introspection
self.requests.append(request)
# parse request data
request_data = {p.split('=')[0]: p.split('=')[1] for p in request.text.split('&')}
# error if the secret or response token is missing
if request_data.get('secret', None) is None or request_data.get('response', None) is None:
response_data = {
'success': False,
'error-codes': ['invalid-input-secret'], # , 'timeout-or-duplicate'
}
# normal, configured response body
else:
response_data = deepcopy(self._next_response)
# send response
context.status_code = 200
return json.dumps(response_data)
# usage:
class RegisterNewAccount(APITestCase):
def setUp(self):
super(RegisterNewAccount, self).setUp()
# prep mock recaptcha
self._mock_recaptcha = MockRecaptcha(self._mocker)
def test_account_registration(self):
pass Beyond this implementation, I suggest the following features for upstream integration into rest-registration:
Thoughts, @apragacz? Btw, @coffeegoddd is going to take over from here! |
Oh, and we obviously should not be consuming the the custom serializer settings in this new implementation. :) |
Hi @joshvillbrandt, |
No rush! We are also considering making a stand-alone django-recaptcha package that could be used as a mixin to any view (or maybe as decorator?). We are open to anything! Thanks for the security note! We will upgrade ASAP. |
Hi, @apragacz! Me and a co-worker are working on integrating Google reCaptcha v3 support with django rest registration. Is this something you would be interested in us massaging into a first-class rest registration feature? If so, we can share our current implementation with you and discuss requirements for turning this into a first-class feature.
Thanks!
CC @coffeegoddd
The text was updated successfully, but these errors were encountered: