Require CAPTCHA for creating new accounts via OAuth

Plugs the remaining hole for bot-based account creation
This commit is contained in:
Joseph Schorr 2017-08-08 15:15:29 -04:00
parent bae9c593ef
commit 57136eb343
2 changed files with 79 additions and 8 deletions

View file

@ -1,7 +1,9 @@
import logging import logging
import time
import recaptcha2
from collections import namedtuple from collections import namedtuple
from flask import request, redirect, url_for, Blueprint from flask import request, redirect, url_for, Blueprint, abort, session
from peewee import IntegrityError from peewee import IntegrityError
import features import features
@ -11,8 +13,8 @@ from auth.auth_context import get_authenticated_user
from auth.decorators import require_session_login from auth.decorators import require_session_login
from data import model from data import model
from endpoints.common import common_login from endpoints.common import common_login
from endpoints.web import index from endpoints.web import index, render_page_template_with_routedata
from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME, generate_csrf_token
from oauth.login import OAuthLoginException from oauth.login import OAuthLoginException
from util.validation import generate_valid_usernames from util.validation import generate_valid_usernames
@ -24,10 +26,12 @@ oauthlogin_csrf_protect = csrf_protect(OAUTH_CSRF_TOKEN_NAME, 'state', all_metho
OAuthResult = namedtuple('oauthresult', ['user_obj', 'service_name', 'error_message', OAuthResult = namedtuple('oauthresult', ['user_obj', 'service_name', 'error_message',
'register_redirect']) 'register_redirect', 'requires_verification'])
def _oauthresult(user_obj=None, service_name=None, error_message=None, register_redirect=False): def _oauthresult(user_obj=None, service_name=None, error_message=None, register_redirect=False,
return OAuthResult(user_obj, service_name, error_message, register_redirect) requires_verification=False):
return OAuthResult(user_obj, service_name, error_message, register_redirect,
requires_verification)
def _get_response(result): def _get_response(result):
if result.error_message is not None: if result.error_message is not None:
@ -35,7 +39,8 @@ def _get_response(result):
return _perform_login(result.user_obj, result.service_name) return _perform_login(result.user_obj, result.service_name)
def _conduct_oauth_login(auth_system, login_service, lid, lusername, lemail, metadata=None): def _conduct_oauth_login(auth_system, login_service, lid, lusername, lemail, metadata=None,
captcha_verified=False):
""" Conducts login from the result of an OAuth service's login flow and returns """ Conducts login from the result of an OAuth service's login flow and returns
the status of the login, as well as the followup step. """ the status of the login, as well as the followup step. """
service_id = login_service.service_id() service_id = login_service.service_id()
@ -85,6 +90,9 @@ def _conduct_oauth_login(auth_system, login_service, lid, lusername, lemail, met
error_message = 'User creation is disabled. Please contact your administrator' error_message = 'User creation is disabled. Please contact your administrator'
return _oauthresult(service_name=service_name, error_message=error_message) return _oauthresult(service_name=service_name, error_message=error_message)
if features.RECAPTCHA and not captcha_verified:
return _oauthresult(service_name=service_name, requires_verification=True)
# Try to create the user # Try to create the user
try: try:
# Generate a valid username. # Generate a valid username.
@ -187,8 +195,17 @@ def _register_service(login_service):
'service_username': lusername, 'service_username': lusername,
} }
# Conduct OAuth login.
captcha_verified = (int(time.time()) - session.get('captcha_verified', 0)) <= 600
session['captcha_verified'] = 0
result = _conduct_oauth_login(authentication, login_service, lid, lusername, lemail, result = _conduct_oauth_login(authentication, login_service, lid, lusername, lemail,
metadata=metadata) metadata=metadata, captcha_verified=captcha_verified)
if result.requires_verification:
return render_page_template_with_routedata('oauthcaptcha.html',
recaptcha_site_key=app.config['RECAPTCHA_SITE_KEY'],
callback_url=request.base_url)
return _get_response(result) return _get_response(result)
@ -215,6 +232,29 @@ def _register_service(login_service):
return redirect(url_for('web.user_view', path=user_obj.username, tab='external')) return redirect(url_for('web.user_view', path=user_obj.username, tab='external'))
def captcha_func():
recaptcha_response = request.values.get('recaptcha_response', '')
result = recaptcha2.verify(app.config['RECAPTCHA_SECRET_KEY'],
recaptcha_response,
request.remote_addr)
if not result['success']:
abort(400)
# Save that the captcha was verified.
session['captcha_verified'] = int(time.time())
# Redirect to the normal OAuth flow again, so that the user can now create an account.
csrf_token = generate_csrf_token(OAUTH_CSRF_TOKEN_NAME)
login_scopes = login_service.get_login_scopes()
auth_url = login_service.get_auth_url(app.config, '', csrf_token, login_scopes)
return redirect(auth_url)
oauthlogin.add_url_rule('/%s/callback/captcha' % login_service.service_id(),
'%s_oauth_captcha' % login_service.service_id(),
captcha_func,
methods=['POST'])
oauthlogin.add_url_rule('/%s/callback' % login_service.service_id(), oauthlogin.add_url_rule('/%s/callback' % login_service.service_id(),
'%s_oauth_callback' % login_service.service_id(), '%s_oauth_callback' % login_service.service_id(),

View file

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}
<title>Confirm · Quay</title>
{% endblock %}
{% block body_content %}
<style type="text/css">
.captcha-container {
text-align: center;
margin-bottom: 20px;
}
.captcha-container div {
display: inline-block;
}
</style>
<div class="container">
<div class="col-sm-6 col-sm-offset-3">
<form name="continueForm" method="post" action="{{ callback_url }}/captcha">
<div class="captcha-container" vc-recaptcha ng-model="recaptcha_response" key="'{{ recaptcha_site_key }}'"></div>
<input type="hidden" name="recaptcha_response" value="{% raw %}{{ recaptcha_response }}{% endraw %}">
<button id="signupButton"
class="btn btn-primary btn-block" ng-disabled="continueForm.$invalid" type="submit">
Continue
</button>
</form>
</div>
</div>
{% endblock %}