Require CAPTCHA for creating new accounts via OAuth
Plugs the remaining hole for bot-based account creation
This commit is contained in:
parent
bae9c593ef
commit
57136eb343
2 changed files with 79 additions and 8 deletions
|
@ -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(),
|
||||||
|
|
31
templates/oauthcaptcha.html
Normal file
31
templates/oauthcaptcha.html
Normal 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 %}
|
Reference in a new issue