Consider performing a full-text search with your search terms.
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:
- Modification of the login form in html/page.html to add the html and javascript required to display the reCAPTCHA.
- reCAPTCHALogin.py is added to the extensions directory
- 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:
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.