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
12 changes: 12 additions & 0 deletions cms/sass/components/_honeypotfield.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.hpemail {
position: absolute;
left: -9999px; /* Large negative offset */
top: -9999px;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0); /* Ensures the field is not visible for sr */
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 @@ -412,14 +412,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('400.html'), 400
Expand Down
13 changes: 4 additions & 9 deletions portality/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,15 +1360,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 @@ -1572,3 +1563,7 @@
BGJOB_MANAGE_REDUNDANT_ACTIONS = [
'read_news', 'journal_csv'
]

##################################################
# Honeypot bot-trap settings for forms (now: only registration form)
HONEYPOT_TIMER_THRESHOLD = 70000;
17 changes: 17 additions & 0 deletions portality/static/js/honeypot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ~~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 honeypot_field_value = $("#email").val();
amdomanska marked this conversation as resolved.
Show resolved Hide resolved
const endTime = performance.now();
const elapsedTime = endTime - doaj.honeypot.startTime;
$("#hptimer").val(elapsedTime);
$("#registrationForm").submit();
}
21 changes: 0 additions & 21 deletions portality/static/js/recaptcha.js

This file was deleted.

46 changes: 23 additions & 23 deletions portality/templates/account/_register_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@

{% from "_formhelpers.html" import render_field %}

<form method="post" action="">
<input type="hidden" name="next" value="/register" />
<div class="form__question">
{% if current_user.is_authenticated and current_user.has_role("create_user") %}
{# 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]") }}
</div>
<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' #}
<input type="text" placeholder="Your email" id="email" name="email" autocomplete="false" tabindex="-1"
class="hpemail" value="">
<div class="form__question">
{% 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) }}
</div>
{{ render_field(form.identifier) }}<br/>
{% 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" />
{{ render_field(form.name, placeholder="Firstname Lastname") }}
</div>
<div class="form__question">
{{ 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) }}
</div>
{% endif %}
{{ render_field(form.next) }}
<input type="hidden" name="hptimer" value="" id="hptimer"/>
<div class="actions">
<input type="submit" id="submitBtn" class="button button--primary" value="Register"/>
</div>
</form>

Expand Down
67 changes: 32 additions & 35 deletions portality/templates/account/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,46 @@
{% block page_title %}Register{% endblock %}

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

{% block content %}
<div class="page-content">
<section class="container">
<div class="row">
<div class="col-md-6">
<h1>Register</h1>
{% if current_user.is_authenticated %}
<p>Create a new user account.</p>
{% else %}
<p>DOAJ is free to use without logging in.</p>
<p>You only need an account if you wish to create an application for a journal’s inclusion in the DOAJ or you are a volunteer.</p>
{% endif %}
<div class="page-content">
<section class="container">
<div class="row">
<div class="col-md-6">
<h1>Register</h1>
{% if current_user.is_authenticated %}
<p>Create a new user account.</p>
{% else %}
<p>DOAJ is free to use without logging in.</p>
<p>You only need an account if you wish to create an application for a journal’s inclusion in
the DOAJ or you are a volunteer.</p>
{% endif %}
</div>
<div class="col-md-6">
{% include "account/_register_form.html" %}
<p>If you have difficulty registering, <a href="/contact">contact us</a>.</p>
</div>
</div>
<div class="col-md-6">
{% include "account/_register_form.html" %}
<p>If you have difficulty registering, <a href="/contact">contact us</a>.</p>
</div>
</div>
</section>
</div>
</section>
</div>
{% endblock %}

{% block extra_js_bottom %}
<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>
<!-- 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 %}
5 changes: 1 addition & 4 deletions portality/templates/doaj/contact.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@
{{ render_field_horizontal(form.email, style="width: 100%; max-width: 400px", placeholder="Please enter your email address") }}
<div style="display: none">{{ render_field_horizontal(form.subject, style="width: 400px; height: 50px", placeholder="Please do not modify this field", autocomplete="off") }}</div>
{{ render_field_horizontal(form.message, style="width: 100%; max-width: 400px; min-height: 200px", placeholder="Enter your message here") }}
{{form.recaptcha_value(id="recaptcha_value")}}
<div class="form-group">
<div class="submit-with-recaptcha col-md-9 col-md-offset-3">
<div class="col-md-9 col-md-offset-3">
<div id="html_element"></div>
<div>
<button id="submitBtn" class="btn btn-success" style="margin-top: 2em;"}>Send</button>
Expand All @@ -59,6 +58,4 @@

{% block extra_js_bottom %}
<script type="text/javascript" src="/static/js/contact.js?v={{config.get('DOAJ_VERSION')}}"></script>
<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>
{% endblock extra_js_bottom %}
Loading
Loading