Add support for recaptcha during the create account flow
If the feature is enabled and recaptcha keys are given in config, then a recaptcha box is displayed in the UI when creating a user and a recaptcha response code *must* be sent with the create API call for it to succeed.
This commit is contained in:
parent
e58e04b0e9
commit
3eb17b7caa
12 changed files with 88 additions and 1 deletions
|
@ -20,7 +20,7 @@ CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
|||
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
||||
'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER', 'DOCUMENTATION_LOCATION',
|
||||
'DOCUMENTATION_METADATA', 'SETUP_COMPLETE', 'DEBUG', 'MARKETO_MUNCHKIN_ID',
|
||||
'STATIC_SITE_BUCKET']
|
||||
'STATIC_SITE_BUCKET', 'RECAPTCHA_SITE_KEY']
|
||||
|
||||
|
||||
def frontend_visible_config(config_dict):
|
||||
|
@ -406,3 +406,8 @@ class DefaultConfig(object):
|
|||
# Example: 10 builds per minute is accomplished by setting ITEMS = 10, SECS = 60
|
||||
MAX_BUILD_QUEUE_RATE_ITEMS = -1
|
||||
MAX_BUILD_QUEUE_RATE_SECS = -1
|
||||
|
||||
# Site key and secret key for using recaptcha.
|
||||
FEATURE_RECAPTCHA = False
|
||||
RECAPTCHA_SITE_KEY = None
|
||||
RECAPTCHA_SECRET_KEY = None
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import logging
|
||||
import json
|
||||
import recaptcha2
|
||||
|
||||
from flask import request, abort
|
||||
from flask_login import logout_user
|
||||
|
@ -183,6 +184,10 @@ class User(ApiResource):
|
|||
'type': 'string',
|
||||
'description': 'The optional invite code',
|
||||
},
|
||||
'recaptcha_response': {
|
||||
'type': 'string',
|
||||
'description': 'The (may be disabled) recaptcha response code for verification',
|
||||
},
|
||||
}
|
||||
},
|
||||
'UpdateUser': {
|
||||
|
@ -382,6 +387,19 @@ class User(ApiResource):
|
|||
abort(404)
|
||||
|
||||
user_data = request.get_json()
|
||||
|
||||
# If recaptcha is enabled, then verify the user is a human.
|
||||
if features.RECAPTCHA:
|
||||
recaptcha_response = user_data.get('recaptcha_response', '')
|
||||
result = recaptcha2.verify(app.config['RECAPTCHA_SECRET_KEY'],
|
||||
recaptcha_response,
|
||||
request.remote_addr)
|
||||
|
||||
if not result['success']:
|
||||
return {
|
||||
'message': 'Are you a bot? If not, please revalidate the captcha.'
|
||||
}, 400
|
||||
|
||||
invite_code = user_data.get('invite_code', '')
|
||||
existing_user = model.user.get_nonrobot_user(user_data['username'])
|
||||
if existing_user:
|
||||
|
|
|
@ -220,6 +220,7 @@ def render_page_template(name, route_data=None, **kwargs):
|
|||
enterprise_logo=app.config.get('ENTERPRISE_LOGO_URL', ''),
|
||||
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||
munchkin_key=app.config.get('MARKETO_MUNCHKIN_ID', ''),
|
||||
recaptcha_key=app.config.get('RECAPTCHA_SITE_KEY', ''),
|
||||
google_tagmanager_key=app.config.get('GOOGLE_TAGMANAGER_KEY', ''),
|
||||
google_anaytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
|
||||
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
||||
|
|
|
@ -18,6 +18,7 @@ EXTERNAL_JS = [
|
|||
'cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0,d3js@3.3.3',
|
||||
'cdn.ravenjs.com/3.1.0/angular/raven.min.js',
|
||||
'cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.min.js',
|
||||
'cdnjs.cloudflare.com/ajax/libs/angular-recaptcha/3.2.1/angular-recaptcha.min.js',
|
||||
]
|
||||
|
||||
EXTERNAL_CSS = [
|
||||
|
|
|
@ -66,3 +66,4 @@ toposort
|
|||
trollius
|
||||
tzlocal
|
||||
xhtml2pdf
|
||||
recaptcha2
|
||||
|
|
|
@ -98,6 +98,7 @@ python-swiftclient==3.1.0
|
|||
pytz==2016.7
|
||||
PyYAML==3.12
|
||||
raven==5.29.0
|
||||
recaptcha2==0.1
|
||||
redis==2.10.5
|
||||
redlock==1.2.0
|
||||
reportlab==2.7
|
||||
|
|
|
@ -10,4 +10,24 @@
|
|||
|
||||
.signup-form-element input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.signup-form-element .captcha {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signup-form-element .captcha div {
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.signup-form-element .captcha {
|
||||
height: 0px;
|
||||
transition: height ease-in-out 250ms;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-setup-element .captcha.expanded {
|
||||
height: 94px;
|
||||
}
|
|
@ -30,6 +30,10 @@
|
|||
match="newUser.password" required
|
||||
ng-pattern="/^.{8,}$/">
|
||||
|
||||
<div class="captcha" quay-require="['RECAPTCHA']"
|
||||
ng-class="{'expanded': newUser.password == newUser.repeatPassword && newUser.password}">
|
||||
<div vc-recaptcha ng-model="newUser.recaptcha_response" key="Config.RECAPTCHA_SITE_KEY"></div>
|
||||
</div>
|
||||
|
||||
<button id="signupButton"
|
||||
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
||||
|
@ -39,6 +43,7 @@
|
|||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="cor-loader" ng-show="registering"></div>
|
||||
<div class="co-alert co-alert-info" ng-show="awaitingConfirmation && hideRegisteredMessage != 'true'">
|
||||
Thank you for registering! We have sent you an activation email.
|
||||
|
|
|
@ -55,6 +55,10 @@ if (window.__config && window.__config.GOOGLE_ANALYTICS_KEY) {
|
|||
quayDependencies.push('angulartics.google.analytics');
|
||||
}
|
||||
|
||||
if (window.__config && window.__config.RECAPTCHA_SITE_KEY) {
|
||||
quayDependencies.push('vcRecaptcha');
|
||||
}
|
||||
|
||||
// Define the application.
|
||||
quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
|
|
|
@ -74,6 +74,10 @@
|
|||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if recaptcha_key %}
|
||||
<script src="//www.google.com/recaptcha/api.js?render=explicit&onload=vcRecaptchaApiLoaded" async defer></script>
|
||||
{% endif %}
|
||||
|
||||
{% if munchkin_key %}
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
|
|
|
@ -9,6 +9,7 @@ import json as py_json
|
|||
|
||||
from contextlib import contextmanager
|
||||
from calendar import timegm
|
||||
from httmock import urlmatch, HTTMock, all_requests
|
||||
from StringIO import StringIO
|
||||
from urllib import urlencode
|
||||
from urlparse import urlparse, urlunparse, parse_qs
|
||||
|
@ -665,6 +666,29 @@ class TestCreateNewUser(ApiTestCase):
|
|||
data = self.postJsonResponse(User, data=NEW_USER_DETAILS, expected_code=200)
|
||||
self.assertEquals(True, data['awaiting_verification'])
|
||||
|
||||
def test_createuser_captcha(self):
|
||||
@urlmatch(netloc=r'(.*\.)?google.com', path='/recaptcha/api/siteverify')
|
||||
def captcha_endpoint(url, request):
|
||||
if url.query.find('response=somecode') > 0:
|
||||
return {'status_code': 200, 'content': py_json.dumps({'success': True})}
|
||||
else:
|
||||
return {'status_code': 400, 'content': py_json.dumps({'success': False})}
|
||||
|
||||
with HTTMock(captcha_endpoint):
|
||||
with self.toggleFeature('RECAPTCHA', True):
|
||||
# Try with a missing captcha.
|
||||
self.postResponse(User, data=NEW_USER_DETAILS, expected_code=400)
|
||||
|
||||
# Try with an invalid captcha.
|
||||
details = dict(NEW_USER_DETAILS)
|
||||
details['recaptcha_response'] = 'someinvalidcode'
|
||||
self.postResponse(User, data=details, expected_code=400)
|
||||
|
||||
# Try with a valid captcha.
|
||||
details = dict(NEW_USER_DETAILS)
|
||||
details['recaptcha_response'] = 'somecode'
|
||||
self.postResponse(User, data=details, expected_code=200)
|
||||
|
||||
def test_createuser_withteaminvite(self):
|
||||
inviter = model.user.get_user(ADMIN_ACCESS_USER)
|
||||
team = model.team.get_organization_team(ORGANIZATION, 'owners')
|
||||
|
|
|
@ -84,3 +84,6 @@ class TestConfig(DefaultConfig):
|
|||
'CLIENT_ID': 'someclientid',
|
||||
'OIDC_SERVER': 'https://oidcserver/',
|
||||
}
|
||||
|
||||
RECAPTCHA_SITE_KEY = 'somekey'
|
||||
RECAPTCHA_SECRET_KEY = 'somesecretkey'
|
||||
|
|
Reference in a new issue