Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/3922 change recaptcha to honeypot #2404

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion cms/sass/components/_alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
.alert {
display: inline-block;
padding: $spacing-03;
margin: $spacing-02 0$spacing-03 0;
margin: $spacing-02 0 $spacing-03 0;
background-color: $light-grey;
border: 1px solid $warm-black;
@include typescale-06;
Expand Down
11 changes: 11 additions & 0 deletions cms/sass/components/_honeypotfield.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.hpemail {
position: absolute;
left: -9999px; /* Large negative offset */
top: -9999px;
height: 1px;
width: 1px;
overflow: hidden;
border: 0;
padding: 0;
margin: 0;
}
3 changes: 2 additions & 1 deletion cms/sass/layout/_page-header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
min-height: 100px;
}

a {
/* do not underline the links, unless in an alert */
a:not(.alert a) {
text-decoration: none;
}

Expand Down
1 change: 1 addition & 0 deletions cms/sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"components/filters",
"components/form",
"components/hero",
"components/honeypotfield",
"components/input-group",
"components/label",
"components/loading",
Expand Down
74 changes: 74 additions & 0 deletions doajtest/fixtures/registrationForm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from werkzeug.datastructures import ImmutableMultiDict, CombinedMultiDict
from portality.core import app

def get_longer_time_than_hp_threshold():
return app.config.get("HONEYPOT_TIMER_THRESHOLD", 5000) + 100

def get_shorter_time_than_hp_threshold():
return app.config.get("HONEYPOT_TIMER_THRESHOLD", 5000) - 100

COMMON = ImmutableMultiDict([
('next', '/register'),
])

VALID_FORM = ImmutableMultiDict([
('name', 'Aga'),
('sender_email', '[email protected]'),
])

INVALID_FORM = ImmutableMultiDict([
('name', 'Aga'),
('sender_email', ''),
])

VALID_HONEYPOT = ImmutableMultiDict([
('email', ''),
('hptimer', get_longer_time_than_hp_threshold())
])

INVALID_HONEYPOT_TIMER_BELOW_THRESHOLD = ImmutableMultiDict([
('email', ''),
('hptimer', get_shorter_time_than_hp_threshold())
])

INVALID_HONEYPOT_EMAIL_NOT_EMPTY = ImmutableMultiDict([
('email', 'this_field@should_be.empty'),
('hptimer', get_shorter_time_than_hp_threshold())
])

INVALID_HONEYPOT_BOTH_FIELDS = ImmutableMultiDict([
('email', 'this_field@should_be.empty'),
('hptimer', get_longer_time_than_hp_threshold())
])

# Method 1: Valid form with valid honeypot
def create_valid_form_with_valid_honeypot():
return CombinedMultiDict([COMMON, VALID_FORM, VALID_HONEYPOT])

# Method 2: Valid form with invalid honeypot (timer exceeds threshold)
def create_valid_form_with_invalid_honeypot_timer_exceeds():
return CombinedMultiDict([COMMON, VALID_FORM, INVALID_HONEYPOT_TIMER_BELOW_THRESHOLD])

# Method 3: Valid form with invalid honeypot (email not empty)
def create_valid_form_with_invalid_honeypot_email_not_empty():
return CombinedMultiDict([COMMON, VALID_FORM, INVALID_HONEYPOT_EMAIL_NOT_EMPTY])

# Method 4: Invalid form with valid honeypot
def create_invalid_form_with_valid_honeypot():
return CombinedMultiDict([COMMON, INVALID_FORM, VALID_HONEYPOT])

# Method 5: Invalid form with invalid honeypot (timer exceeds threshold)
def create_invalid_form_with_invalid_honeypot_timer_exceeds():
return CombinedMultiDict([COMMON, INVALID_FORM, INVALID_HONEYPOT_TIMER_BELOW_THRESHOLD])

# Method 6: Invalid form with invalid honeypot (email not empty)
def create_invalid_form_with_invalid_honeypot_email_not_empty():
return CombinedMultiDict([COMMON, INVALID_FORM, INVALID_HONEYPOT_EMAIL_NOT_EMPTY])

# Method 7: Valid form with invalid honeypot (both fields)
def create_valid_form_with_invalid_honeypot_both_fields():
return CombinedMultiDict([COMMON, VALID_FORM, INVALID_HONEYPOT_BOTH_FIELDS])

# Method 8: Invalid form with invalid honeypot (both fields)
def create_invalid_form_with_invalid_honeypot_both_fields():
return CombinedMultiDict([COMMON, INVALID_FORM, INVALID_HONEYPOT_BOTH_FIELDS])
41 changes: 41 additions & 0 deletions doajtest/unit/test_honeypot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from doajtest.helpers import DoajTestCase
from doajtest.fixtures import registrationForm
from portality.view.account import RegisterForm
from werkzeug.datastructures import ImmutableMultiDict

class TestHoneypot(DoajTestCase):

def setUp(self):
pass

def test_01_valid_form_with_valid_honeypot(self):
valid_form = RegisterForm(registrationForm.create_valid_form_with_valid_honeypot())
assert valid_form.is_bot() is False, "Test failed: The form should not be identified as a bot."

def test_02_valid_form_with_invalid_honeypot_timer_exceeds(self):
valid_form = RegisterForm(registrationForm.create_valid_form_with_invalid_honeypot_timer_exceeds())
assert valid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot timer exceeding threshold."

def test_03_valid_form_with_invalid_honeypot_email_not_empty(self):
valid_form = RegisterForm(registrationForm.create_valid_form_with_invalid_honeypot_email_not_empty())
assert valid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot email field not being empty."

def test_04_invalid_form_with_valid_honeypot(self):
invalid_form = RegisterForm(registrationForm.create_invalid_form_with_valid_honeypot())
assert invalid_form.is_bot() is False, "Test failed: The form should not be identified as a bot since honeypot is valid."

def test_05_invalid_form_with_invalid_honeypot_timer_exceeds(self):
invalid_form = RegisterForm(registrationForm.create_invalid_form_with_invalid_honeypot_timer_exceeds())
assert invalid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot timer exceeding threshold."

def test_06_invalid_form_with_invalid_honeypot_email_not_empty(self):
invalid_form = RegisterForm(registrationForm.create_invalid_form_with_invalid_honeypot_email_not_empty())
assert invalid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot email field not being empty."

def test_07_valid_form_with_invalid_honeypot_both_fields(self):
valid_form = RegisterForm(registrationForm.create_valid_form_with_invalid_honeypot_both_fields())
assert valid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to both honeypot fields being invalid."

def test_08_invalid_form_with_invalid_honeypot_both_fields(self):
invalid_form = RegisterForm(registrationForm.create_invalid_form_with_invalid_honeypot_both_fields())
assert invalid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to both honeypot fields being invalid."
8 changes: 0 additions & 8 deletions portality/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,14 +413,6 @@ def api_directory():
)
return jsonify({'api_versions': vers})


# Make the reCAPTCHA key available to the js
# ~~-> ReCAPTCHA:ExternalService~~
@app.route('/get_recaptcha_site_key')
def get_site_key():
return app.config.get('RECAPTCHA_SITE_KEY', '')


@app.errorhandler(400)
def page_not_found(e):
return render_template(templates.ERROR_400), 400
Expand Down
13 changes: 4 additions & 9 deletions portality/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,15 +1342,6 @@
# assume it's a zombie, and ignore it
HARVESTER_ZOMBIE_AGE = 604800

#######################################################
# ReCAPTCHA configuration
# ~~->ReCAPTCHA:ExternalService

#Recaptcha test keys, should be overridden in dev.cfg by the keys obtained from Google ReCaptcha v2
RECAPTCHA_ENABLE = True
RECAPTCHA_SITE_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"

#######################################################
# Preservation configuration
# ~~->Preservation:Feature
Expand Down Expand Up @@ -1554,3 +1545,7 @@
BGJOB_MANAGE_REDUNDANT_ACTIONS = [
'read_news', 'journal_csv'
]

##################################################
# Honeypot bot-trap settings for forms (now: only registration form)
HONEYPOT_TIMER_THRESHOLD = 5000;
18 changes: 18 additions & 0 deletions portality/static/js/honeypot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// ~~Honeypot:Feature~~
doaj.honeypot = {}

doaj.honeypot.init = function () {
console.log("init");
doaj.honeypot.startTime = performance.now();
$("#submitBtn").on("click", (event) => doaj.honeypot.handleRegistration(event));
}

doaj.honeypot.handleRegistration = function (event) {
event.preventDefault();
const endTime = performance.now();
const elapsedTime = endTime - doaj.honeypot.startTime;
// reset the timer
doaj.honeypot.startTime = performance.now();
$("#hptimer").val(elapsedTime);
$("#registrationForm").submit();
}
21 changes: 0 additions & 21 deletions portality/static/js/recaptcha.js

This file was deleted.

29 changes: 15 additions & 14 deletions portality/templates-v2/_account/includes/_register_form.html
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
{% from "includes/_formhelpers.html" import render_field %}

<form method="post" action="">
<input type="hidden" name="next" value="/register" />
<form method="post" action="" id="registrationForm">
<input type="hidden" name="next" value="/register"/>
{# This input is a bot-bait, it should stay invisible to the users and empty. #}
{# Make sure it's invisible on the screen AND FOR SCREEN READERS/KEYBOARD USERS' #}
<div aria-hidden="true" class="hpemail">
<input type="text" id="email" name="email" autocomplete="false" tabindex="-1" value="">
</div>
<div class="form__question">
{% if current_user.is_authenticated and current_user.has_role("create_user") %}
{# Admins can specify a user ID #}
{# Admins can specify a user ID #}
{{ render_field(form.identifier) }}<br/>
{% endif %}
{{ render_field(form.name, placeholder="Firstname Lastname") }}
</div>
<div class="form__question">
{{ render_field(form.email, placeholder="[email protected]") }}
{{ render_field(form.sender_email, placeholder="[email protected]") }}
</div>
{% if current_user.is_authenticated and current_user.has_role("create_user") %}
{# Admins can specify a user ID #}
<div class="form__question">
{{ render_field(form.roles) }}
{{ render_field(form.roles) }}
</div>
{% endif %}

<div class=form__question id="recaptcha_div"></div>

{{ render_field(form.next) }}
{{form.recaptcha_value(id="recaptcha_value")}}

<div class="submit-with-recaptcha actions">
<input type="submit" id="submitBtn" class="button button--primary" value="Register" />
</div>
</form>
<input type="hidden" name="hptimer" value="" id="hptimer"/>
<div class="actions">
<input type="submit" id="submitBtn" class="button button--primary" value="Register"/>
</div>
</form>
21 changes: 8 additions & 13 deletions portality/templates-v2/management/admin/account/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@

{% block page_title %}Create User{% endblock %}

{% block admin_stylesheets %}
<style>
#recaptcha_div iframe {
padding: unset;
border: unset;
}
</style>
{% endblock %}
{% block admin_stylesheets %}{% endblock %}

{% block admin_content %}
<div class="page-content">
Expand All @@ -24,18 +17,20 @@
{% endblock %}

{% block admin_js %}
<script type="text/javascript" src="/static/js/honeypot.js?v={{ config.get('DOAJ_VERSION') }}"></script>
{% if current_user.is_authenticated and current_user.has_role("create_user") %}
<!-- select2 for role picker on admin create user form -->
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#roles').select2({tags:["{{current_user.all_top_level_roles()|join('","')|safe}}"],width:'70%'});
});
</script>
{% endif %}

{% if config.get("RECAPTCHA_ENABLE") %}
<!-- reCAPTCHA for the register form -->
<script type="text/javascript" src="/static/js/recaptcha.js?v={{config.get('DOAJ_VERSION')}}"></script>
<script src="https://www.recaptcha.net/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
{% else %}
<script type="text/javascript">
jQuery(document).ready(function ($) {
doaj.honeypot.init();
});
</script>
{% endif %}
{% endblock %}
21 changes: 8 additions & 13 deletions portality/templates-v2/public/account/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@

{% block page_title %}Register{% endblock %}

{% block public_stylesheets %}
<style>
#recaptcha_div iframe {
padding: unset;
border: unset;
}
</style>
{% endblock %}
{% block public_stylesheets %}{% endblock %}

{% block public_content %}
<div class="page-content">
Expand All @@ -34,18 +27,20 @@ <h1>Register</h1>
{% endblock %}

{% block public_js %}
<script type="text/javascript" src="/static/js/honeypot.js?v={{ config.get('DOAJ_VERSION') }}"></script>
{% if current_user.is_authenticated and current_user.has_role("create_user") %}
<!-- select2 for role picker on admin create user form -->
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#roles').select2({tags:["{{current_user.all_top_level_roles()|join('","')|safe}}"],width:'70%'});
});
</script>
{% endif %}

{% if config.get("RECAPTCHA_ENABLE") %}
<!-- reCAPTCHA for the register form -->
<script type="text/javascript" src="/static/js/recaptcha.js?v={{config.get('DOAJ_VERSION')}}"></script>
<script src="https://www.recaptcha.net/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
{% else %}
<script type="text/javascript">
jQuery(document).ready(function ($) {
doaj.honeypot.init();
});
</script>
{% endif %}
{% endblock %}
2 changes: 2 additions & 0 deletions portality/ui/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ class Messages(object):

PRESERVATION_NO_FILE = "No file provided for upload"

ARE_YOU_A_HUMAN = "Are you sure you're a human? If you're having trouble logging in, please <a href='/contact'>contact us</a>."

@classmethod
def flash(cls, tup):
if isinstance(tup, tuple):
Expand Down
11 changes: 0 additions & 11 deletions portality/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,6 @@ def ipt_prefix(type):
return type


def verify_recaptcha(g_recaptcha_response):
"""
~~ReCAPTCHA:ExternalService~~
:param g_recaptcha_response:
:return:
"""
with urllib.request.urlopen('https://www.recaptcha.net/recaptcha/api/siteverify?secret=' + app.config.get("RECAPTCHA_SECRET_KEY") + '&response=' + g_recaptcha_response) as url:
data = json.loads(url.read().decode())
return data


def url_for(*args, **kwargs):
"""
This function is a hack to allow us to use url_for where we may nor may not have the
Expand Down
Loading
Loading