Roundup Tracker

Your search query "linkto%3A%22RequireReCAPTCHAForLogin%22" didn't return any results. Please change some terms and refer to HelpOnSearching for more information.
(!) Consider performing a full-text search with your search terms.

Clear message

This extension adds reCAPTCHA (v2 or v3) (https://developers.google.com/recaptcha/) to the login form. This helps prevent password guessing by automated login attempts.

(It reportedly also works with Cloudflare Turnstyle. Directions for migrating are at: https://developers.cloudflare.com/turnstile/migration/migrating-from-recaptcha/. The url's used in the html form and in the backend have to change. You should use the recaptcha v2 config. If anybody makes the URL's configurable so this can just drop in and only changes to extenstion/config.ini have to be made, they would be welcome.)

There are three parts to the extension:

  1. Modification of the login form in html/page.html to add the html and javascript required to display the reCAPTCHA.
  2. reCAPTCHALogin.py is added to the extensions directory
  3. config.ini which is added to the extension directory and used to enable and control the reCAPTCHA module.

This code improves the original code by including support for reCAPTCHA v2 and v3.

Changes to the html/page.html login form

Put this block of html just before the login button in html/page.html

    <!-- recaptcha block -->
    <tal:if tal:condition="python:getattr(db.config.ext,
      'RECAPTCHA_SECRET', '') not in ('', 'none')"
    tal:define="reCaptcha_version
             python:getattr(db.config.ext,
                   'RECAPTCHA_VERSION', 'v2');
                       sitekey python:getattr(db.config.ext,
                        'RECAPTCHA_SITEKEY', '')">
      <tal:if tal:condition="python:reCaptcha_version == 'v2'">
        <div class="g-recaptcha"
           tal:attributes="data-sitekey string:$sitekey"
           data-size="compact"></div>
        <script 
         tal:attributes="nonce request/client/client_nonce"
         src="https://www.google.com/recaptcha/api.js"
         async defer></script>
      </tal:if>
      <tal:if tal:condition="python:reCaptcha_version == 'v3'">
        <input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response">
        <script
           tal:attributes="nonce request/client/client_nonce;src string:https://www.google.com/recaptcha/api.js?render=$sitekey">
        </script>
        <script tal:attributes="nonce request/client/client_nonce"
          tal:content="string:     grecaptcha.ready(function() {
             grecaptcha.execute(
                  '$sitekey',
                  {action:'login'}).then(function(token) {
                     document.getElementById(
                         'g-recaptcha-response').value = token;
                     }); 
          });">
        </script>
      </tal:if>
      <noscript>
        <div>
          <hr>
          Please enable JavaScript. Then solve the
          test that helps keep your account safe.
          <hr>
        </div>
      </noscript>
    </tal:if>

You should delete the attribute tal:attributes that sets the nonce if you are not running version 1.6.0 or newer of roundup (1.6.0 is the development release at the time this page was created).

Put the block above just before the line that looks something like:

  <input type="submit" name="Login" value="Login" i18n:attributes="value"><br>

Python extension reCAPTCHAlogin.py to put in extensions subdirectory of tracker

Put the contents of this area into a .py file in the extensions directory of the tracker. E.G. reCAPTCHAlogin.py.

Because white space is mangled by the wiki, copy it in raw mode.

   1 from roundup.cgi.actions import LoginAction
   2 from roundup.exceptions import Reject, RejectRaw
   3 from roundup.anypy.http_ import client
   4 
   5 from urlparse import urlparse
   6 import json
   7 
   8 import logging
   9 logger = logging.getLogger('actions')
  10 
  11 class reCAPTCHAloginAction(LoginAction):
  12     '''Login action requiring reCAPTCHA before username/password checked
  13 
  14        Set recaptcha 'secret' in the [recaptcha] section of
  15        extensions/config.ini to the secret to enable recaptcha.
  16        Set the 'sitekey' in the same section to the site key.
  17        See:
  18          https://developers.google.com/recaptcha/
  19        for details.
  20 
  21        This code validates the passed in recaptcha code that is generated
  22        by the recaptcha javascript.
  23     '''
  24 
  25     # URL for the validation API.
  26     url = "https://www.google.com/recaptcha/api/siteverify"
  27 
  28     def handle(self):
  29         ''' Implement alternate login
  30 
  31             Things to improve:
  32                generate email if reCAPTCHA validation fails.
  33                handle timeout of reCAPTCHA better. Right now throws
  34                  and emails traceback.
  35         '''
  36 
  37         if __debug__:
  38             logger.debug("reCAPTCHAloginAction: enter")
  39 
  40         secret = getattr(self.db.config.ext, 'RECAPTCHA_SECRET', False)
  41         # use default of 0.5 as recommended by google.
  42         # value below this threshold is probably not a human
  43         score_threshold = getattr(self.db.config.ext, 'RECAPTCHA_SCORE',
  44                                   "0.5")
  45         version = getattr(self.db.config.ext, 'RECAPTCHA_VERSION', "v2")
  46 
  47         local_net_prefix="192.168"
  48         if 'REMOTE_ADDR' in self.client.env:
  49             # if client at local site, don't validate captcha
  50             # used for running automated tests. 
  51             if self.client.env['REMOTE_ADDR'].startswith(local_net_prefix):
  52                 secret=None
  53 
  54         if 'HTTP_X-FORWARDED-FOR' in self.client.env:
  55             # if proxied from client at local site, don't validate captcha
  56             # used for running automated tests.
  57             clientip=self.client.env['HTTP_X-FORWARDED-FOR'].split(',')[0]
  58             if clientip.startswith(local_net_prefix):
  59                 secret=None
  60 
  61         # if secret missing or set to none skip recaptcha
  62         if secret and secret not in ("", None):
  63             if '__login_name' in self.form:
  64                 login=self.form['__login_name']
  65             else:
  66                 login=None
  67 
  68             if __debug__:
  69                 logger.debug("reCAPTCHAloginAction: validating version %s user %s w/ secret=%s", version, login, secret)
  70 
  71             if 'g-recaptcha-response' not in self.form:
  72                 raise Reject(self._('Missing reCAPTCHA response.'))
  73 
  74             recaptcha_solution = self.form['g-recaptcha-response'].value
  75                 
  76             data = 'secret=%s&response=%s'%(secret,recaptcha_solution)
  77             headers = {"Content-type": "application/x-www-form-urlencoded",
  78                        "Accept": "text/plain"}
  79 
  80             urlparts = urlparse(self.url)
  81             conn = client.HTTPSConnection(urlparts.netloc,
  82                                     urlparts.port or 443, timeout=5)
  83             # enable to trace the http payload
  84             # conn.set_debuglevel(1)
  85             conn.request("POST", urlparts.path, data, headers)
  86             resp = conn.getresponse()
  87             json_response = resp.read()
  88             response = json.loads(json_response)
  89 
  90             if __debug__:
  91                 logger.debug("reCAPTCHAloginAction: %s", response)
  92 
  93             # reCaptcha v2, success = true means a human
  94             # reCaptcha v3, success = true means the request was a valid token
  95             # in either case False is a failure.
  96             if response['success'] is False:
  97                 logger.error("reCAPTCHAloginAction: Validation failed for user %s, response: %s", login, response)
  98                 raise Reject(self._('reCAPTCHA validation failed.'))
  99 
 100             # start google reCaptcha v3
 101             if version == "v3":
 102                 ''' action and score must both be defined, if either is
 103                     missing fail validation.
 104                 '''
 105                 action=None
 106                 score=None
 107 
 108                 if 'action' in response:
 109                     action = response['action']
 110                 if 'score' in response:
 111                     score = response['score']
 112 
 113                 if action != 'login':
 114                     logger.error("reCAPTCHAloginAction: Validation failed for user %s, action %s does not match 'login'.", login, action)
 115                     raise Reject(self._('reCAPTCHA validation failed.'))
 116 
 117                 if not (score and \
 118                         float(score) > float(score_threshold)):
 119                     # fail if score is None or score < score_threshold
 120                     logger.error("reCAPTCHAloginAction: Validation failed for user %s, score less than threshold: %s < %s", login, score, score_threshold)
 121                     raise Reject(self._('reCAPTCHA validation failed.'))
 122             # end google reCaptcha v3
 123 
 124         # call the core LoginAction to process the login username/password.
 125         LoginAction.handle(self)
 126 
 127 
 128 def init(instance):
 129     '''Override the default login action with this new version'''
 130     instance.registerAction('login', reCAPTCHAloginAction)

You may want to edit the local_net_prefix to a network suitable for your network, or to 0. to disable.

config.ini to put in extensions subdirectory of tracker

Add this to the end of the extensions/config.ini file. Create the file if required.

[recaptcha]
# Configure the reCAPTCHAlogin extension module
# Adds a reCAPTCHA as part of the login form.
# See: https://developers.google.com/recaptcha/

# reCAPTCHA version: v2 (default) or v3
# version=v2

# secret key - if setting is commented out/missing or
# value is empty or value is set to none, module is disabled.
# secret = none

# Must set to the site key matching the secret key above.
# sitekey = 

# If using google's reCAPTCHA v3, use this to set the passing score
# above/below 0.5 which is the default.
# score = 0.5

To enable reCAPTCHA, uncomment the sitekey and secret lines and insert the codes you got when you signed up for reCAPTCHA at google. If you are using v3, uncomment the version line and set it to "v3". If you are using v3, and you want a higher/lower threshold than 0.5, you can set it with the score line.

Then restart your tracker instance.

You should see something like the following:

login_w_recaptcha.png

for version 2. If the user does not complete the reCAPTCHA the login is denied.

Version 3 of reCAPTCHA does not display anything to the user. It looks at how the form is used to determine if the user is a bot or human. There is a small reCAPTCHA logo on the bottom right indicating that it is in use. Hovering over it gives more info. If you want to hide it (and keep within the terms of service), see some options here: https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge.

The result of the reCAPTCHA is validated using the Google validation api. If that is not available, the login attempt will fail.

The code needs some polish. E.G. some way to limit the time spent trying to validate the reCAPTCHA should be added. Currently it's possible an exception may be raised if the validation times out. Also an option should be added (set in the exceptions/config.ini [recaptcha] section) to enable completing the login if the validation times out or errors out.


CategoryActions CategoryAuthentication