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 time
import recaptcha2
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
import features
@ -11,8 +13,8 @@ from auth.auth_context import get_authenticated_user
from auth.decorators import require_session_login
from data import model
from endpoints.common import common_login
from endpoints.web import index
from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME
from endpoints.web import index, render_page_template_with_routedata
from endpoints.csrf import csrf_protect, OAUTH_CSRF_TOKEN_NAME, generate_csrf_token
from oauth.login import OAuthLoginException
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',
'register_redirect'])
'register_redirect', 'requires_verification'])
def _oauthresult(user_obj=None, service_name=None, error_message=None, register_redirect=False):
return OAuthResult(user_obj, service_name, error_message, register_redirect)
def _oauthresult(user_obj=None, service_name=None, error_message=None, register_redirect=False,
requires_verification=False):
return OAuthResult(user_obj, service_name, error_message, register_redirect,
requires_verification)
def _get_response(result):
if result.error_message is not None:
@ -35,7 +39,8 @@ def _get_response(result):
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
the status of the login, as well as the followup step. """
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'
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:
# Generate a valid username.
@ -187,8 +195,17 @@ def _register_service(login_service):
'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,
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)
@ -215,6 +232,29 @@ def _register_service(login_service):
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(),
'%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 %}