Skip to content
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

Open
joshvillbrandt opened this issue Jun 26, 2019 · 6 comments
Open

Google reCaptcha Support #61

joshvillbrandt opened this issue Jun 26, 2019 · 6 comments

Comments

@joshvillbrandt
Copy link

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

@joshvillbrandt joshvillbrandt changed the title Google reCaptcha Google reCaptcha Support Jun 26, 2019
@apragacz
Copy link
Owner

Hi @joshvillbrandt,
yes, actually I already created issue #37 to research this topic, but didn't have time to delve into it. I'm certainly interested to see your solution! Of course I can't ensure that it will be merged directly but I really hope it could be made into something all Django REST Registration users can benefit from.

@joshvillbrandt
Copy link
Author

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!

@joshvillbrandt
Copy link
Author

joshvillbrandt commented Jun 28, 2019

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 register/, send-reset-password-link/, and login/ endpoints. We had these additions to our settings file:

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 (recaptchaToken) to be sent along with their requests. This token is retrieved on the front end using the grecaptcha library as explained in the reCaptcha v3 docs. Additionally, we set the "action" individually for each endpoint so that reCaptcha's internal analytics can track each type of request separately. The action for each endpoint is essentially the endpoint URL with the exception that dashes aren't supported by reCaptcha, so we used underscores instead.

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:

  • Do we want to expand this to all of the endpoints?
  • REST_REGISTRATION.RECAPTCHA_ENABLED Enables this whole mess. Defaults to False.
  • REST_REGISTRATION.RECAPTCHA_ACTION_PREFIX Namespaces the action values coming from rest registration in case rest registration is used elsewhere on the site. Let's say this defaults to "reg_".
  • Maybe a per-endpoint setting to enable/disable reCaptcha.
  • We used the python requests library to make our reCaptcha API call. Maybe you want to switch to urllib instead to minimize dependencies? We'll have to figure out how to mock with urllib.
  • Would be neat to make this easily available for other rest endpoints......... Hmm! How could we do this? Maybe too much for now.

Thoughts, @apragacz? Btw, @coffeegoddd is going to take over from here!

@joshvillbrandt
Copy link
Author

Oh, and we obviously should not be consuming the the custom serializer settings in this new implementation. :)

@apragacz
Copy link
Owner

apragacz commented Jul 1, 2019

Hi @joshvillbrandt,
thanks for your input. I still need some time to parse it, I should give you some reasonable answer within few days.
In the meantime, I highly recommend to upgrade the Django REST Registration version to 0.5.0. there was a security issue found (IMO high severity one), you can read more about that here.

@joshvillbrandt
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants