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

Merged
merged 16 commits into from
Feb 7, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms/sass/components/_alert.scss
Original file line number Diff line number Diff line change
@@ -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;
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
@@ -7,7 +7,8 @@
min-height: 100px;
}

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

1 change: 1 addition & 0 deletions cms/sass/main.scss
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@
"components/filters",
"components/form",
"components/hero",
"components/honeypotfield",
"components/input-group",
"components/label",
"components/loading",
4 changes: 4 additions & 0 deletions portality/settings.py
Original file line number Diff line number Diff line change
@@ -1563,3 +1563,7 @@
BGJOB_MANAGE_REDUNDANT_ACTIONS = [
'read_news', 'journal_csv'
]

##################################################
# Honeypot bot-trap settings for forms (now: only registration form)
HONEYPOT_TIMER_THRESHOLD = 7000;
16 changes: 16 additions & 0 deletions portality/static/js/honeypot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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();
}
41 changes: 22 additions & 19 deletions portality/templates/account/_register_form.html
Original file line number Diff line number Diff line change
@@ -2,29 +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 %}

{{ render_field(form.next) }}

{{ 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" />
<input type="submit" id="submitBtn" class="button button--primary" value="Register"/>
</div>
</form>

58 changes: 33 additions & 25 deletions portality/templates/account/register.html
Original file line number Diff line number Diff line change
@@ -6,35 +6,43 @@
{% 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 %}
{% else %}
<script type="text/javascript">
jQuery(document).ready(function ($) {
doaj.honeypot.init();
});
</script>
{% endif %}
{% endblock %}
56 changes: 34 additions & 22 deletions portality/view/account.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
from flask import Blueprint, request, url_for, flash, redirect, make_response
from flask import render_template, abort
from flask_login import login_user, logout_user, current_user, login_required
from wtforms import StringField, HiddenField, PasswordField, validators, Form
from wtforms import StringField, HiddenField, PasswordField, IntegerField, validators, Form

from portality import util
from portality import constants
@@ -308,15 +308,23 @@ def logout():
class RegisterForm(RedirectForm):
identifier = StringField('ID', [ReservedUsernames(), IdAvailable()])
name = StringField('Name', [validators.Optional(), validators.Length(min=3, max=64)])
email = StringField('Email address', [
sender_email = StringField('Email address', [
validators.DataRequired(),
validators.Length(min=3, max=254),
validators.Email(message='Must be a valid email address'),
EmailAvailable(message="That email address is already in use. Please <a href='/account/forgot'>reset your password</a>. If you still cannot login, <a href='/contact'>contact us</a>.")
])
roles = StringField('Roles')
# recaptcha_value = HiddenField()
# These are honeypot (bot-trap) fields
email = StringField('email')
hptimer = IntegerField('hptimer')
amdomanska marked this conversation as resolved.
Show resolved Hide resolved

def is_bot(self):
"""
Checks honeypot fields and determines whether the form was submitted by a bot
:return: True, if bot suspected; False, if human
"""
return (self.email.data != "" or self.hptimer.data < app.config.get("HONEYPOT_TIMER_THRESHOLD", 5000))

@blueprint.route('/register', methods=['GET', 'POST'])
@ssl_required
@@ -328,31 +336,35 @@ def register():
abort(401) # todo: we may need a template to explain this since it's linked from the application form

form = RegisterForm(request.form, csrf_enabled=False, roles='api,publisher', identifier=Account.new_short_uuid())
if request.method == 'POST' and form.validate():

account = Account.make_account(email=form.email.data, username=form.identifier.data, name=form.name.data,
roles=[r.strip() for r in form.roles.data.split(',')])
account.save()
if request.method == 'POST' and not form.is_bot():

event_svc = DOAJ.eventsService()
event_svc.trigger(Event(constants.EVENT_ACCOUNT_CREATED, account.id, context={"account" : account.data}))
# send_account_created_email(account)
if form.validate():
account = Account.make_account(email=form.sender_email.data, username=form.identifier.data, name=form.name.data,
roles=[r.strip() for r in form.roles.data.split(',')])
account.save()

if app.config.get('DEBUG', False):
util.flash_with_url('Debug mode - url for verify is <a href={0}>{0}</a>'.format(url_for('account.reset', reset_token=account.reset_token)))
event_svc = DOAJ.eventsService()
event_svc.trigger(Event(constants.EVENT_ACCOUNT_CREATED, account.id, context={"account" : account.data}))
# send_account_created_email(account)

if current_user.is_authenticated:
util.flash_with_url('Account created for {0}. View Account: <a href={1}>{1}</a>'.format(account.email, url_for('.username', username=account.id)))
return redirect(url_for('.index'))
else:
flash('Thank you, please verify email address ' + form.email.data + ' to set your password and login.',
'success')
if app.config.get('DEBUG', False):
util.flash_with_url('Debug mode - url for verify is <a href={0}>{0}</a>'.format(url_for('account.reset', reset_token=account.reset_token)))

# We must redirect home because the user now needs to verify their email address.
return redirect(url_for('doaj.home'))
if current_user.is_authenticated:
util.flash_with_url('Account created for {0}. View Account: <a href={1}>{1}</a>'.format(account.email, url_for('.username', username=account.id)))
return redirect(url_for('.index'))
else:
flash('Thank you, please verify email address ' + form.sender_email.data + ' to set your password and login.',
'success')

# We must redirect home because the user now needs to verify their email address.
return redirect(url_for('doaj.home'))
else:
flash('Please correct the errors', 'error')

if request.method == 'POST' and form.is_bot():
flash(
f"Are you sure you're a human? If you're having trouble logging in, please <a href='/contact'>contact us</a>. {form.email.data, form.hptimer.data}", "error")

if request.method == 'POST' and not form.validate():
flash('Please correct the errors', 'error')
return render_template('account/register.html', form=form)