Introduce honeypot for login/registering/password changing

This commit is contained in:
Antoine Bartuccio 2024-07-10 12:24:41 +02:00
parent 7de2e00c94
commit 72cf5a3d5e
9 changed files with 132 additions and 17 deletions

View File

@ -33,6 +33,7 @@
{% endif %} {% endif %}
{% csrf_token %} {% csrf_token %}
{% render_honeypot_field %}
<div> <div>
<label for="{{ form.username.name }}">{{ form.username.label }}</label> <label for="{{ form.username.name }}">{{ form.username.label }}</label>

View File

@ -3,6 +3,7 @@
{% block content %} {% block content %}
<form method="post" action=""> <form method="post" action="">
{% csrf_token %} {% csrf_token %}
{% render_honeypot_field %}
{{ form.as_p() }} {{ form.as_p() }}
<input type="submit" value="{% trans %}Reset{% endtrans %}" /> <input type="submit" value="{% trans %}Reset{% endtrans %}" />
</form> </form>

View File

@ -23,6 +23,7 @@
{% else %} {% else %}
<form action="{{ url('core:register') }}" method="post"> <form action="{{ url('core:register') }}" method="post">
{% csrf_token %} {% csrf_token %}
{% render_honeypot_field %}
{{ form }} {{ form }}
<input type="submit" value="{% trans %}Register{% endtrans %}" /> <input type="submit" value="{% trans %}Register{% endtrans %}" />
</form> </form>

View File

@ -0,0 +1,58 @@
#
# Copyright 2024
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from typing import Callable
import honeypot.templatetags.honeypot as honeypot_filters
from django.template.loader import render_to_string
from jinja2 import Environment, nodes
from jinja2.ext import Extension
from jinja2.parser import Parser
class HoneypotExtension(Extension):
"""
Wrapper around the honeypot extension tag
Known limitation: doesn't support arguments
Usage: {% render_honeypot_field %}
"""
tags = {"render_honeypot_field"}
def __init__(self, environment: Environment) -> None:
environment.globals["render_honeypot_field"] = (
honeypot_filters.render_honeypot_field
)
self.environment = environment
def parse(self, parser: Parser) -> nodes.Output:
lineno = parser.stream.expect("name:render_honeypot_field").lineno
call = self.call_method(
"_render",
[nodes.Name("render_honeypot_field", "load", lineno=lineno)],
lineno=lineno,
)
return nodes.Output([nodes.MarkSafe(call)])
def _render(self, render_honeypot_field: Callable[[str | None], str]):
return render_to_string("honeypot/honeypot_field.html", render_honeypot_field())

View File

@ -36,6 +36,7 @@ class TestUserRegistration:
@pytest.fixture() @pytest.fixture()
def valid_payload(self): def valid_payload(self):
return { return {
settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE,
"first_name": "this user does not exist (yet)", "first_name": "this user does not exist (yet)",
"last_name": "this user does not exist (yet)", "last_name": "this user does not exist (yet)",
"email": "i-dont-exist-yet@git.an", "email": "i-dont-exist-yet@git.an",
@ -68,6 +69,13 @@ class TestUserRegistration:
assert response.status_code == 200 assert response.status_code == 200
assert "TEST_REGISTER_USER_FORM_FAIL" in str(response.content) assert "TEST_REGISTER_USER_FORM_FAIL" in str(response.content)
def test_register_honeypot_fail(self, client, valid_payload):
payload = valid_payload | {
settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE + "random"
}
response = client.post(reverse("core:register"), payload)
assert response.status_code == 400
def test_register_user_form_fail_already_exists(self, client, valid_payload): def test_register_user_form_fail_already_exists(self, client, valid_payload):
"""Should not register a user correctly if it already exists.""" """Should not register a user correctly if it already exists."""
# create the user, then try to create it again # create the user, then try to create it again
@ -90,7 +98,11 @@ class TestUserLogin:
response = client.post( response = client.post(
reverse("core:login"), reverse("core:login"),
{"username": user.username, "password": "wrong-password"}, {
"username": user.username,
"password": "wrong-password",
settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE,
},
) )
assert response.status_code == 200 assert response.status_code == 200
assert ( assert (
@ -98,12 +110,28 @@ class TestUserLogin:
"et votre mot de passe ne correspondent pas. Merci de réessayer.</p>" "et votre mot de passe ne correspondent pas. Merci de réessayer.</p>"
) in str(response.content.decode()) ) in str(response.content.decode())
def test_login_honeypot(self, client, user):
response = client.post(
reverse("core:login"),
{
"username": user.username,
"password": "wrong-password",
settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE + "incorrect",
},
)
assert response.status_code == 400
def test_login_success(self, client, user): def test_login_success(self, client, user):
""" """
Should login a user correctly Should login a user correctly
""" """
response = client.post( response = client.post(
reverse("core:login"), {"username": user.username, "password": "plop"} reverse("core:login"),
{
"username": user.username,
"password": "plop",
settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE,
},
) )
assertRedirects(response, reverse("core:index")) assertRedirects(response, reverse("core:index"))

View File

@ -36,6 +36,7 @@ from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
@ -46,6 +47,7 @@ from django.views.generic import (
) )
from django.views.generic.dates import MonthMixin, YearMixin from django.views.generic.dates import MonthMixin, YearMixin
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from honeypot.decorators import check_honeypot
from api.views.sas import all_pictures_of_user from api.views.sas import all_pictures_of_user
from core.models import Gift, Preferences, SithFile, User from core.models import Gift, Preferences, SithFile, User
@ -69,6 +71,7 @@ from subscription.models import Subscription
from trombi.views import UserTrombiForm from trombi.views import UserTrombiForm
@method_decorator(check_honeypot, name="post")
class SithLoginView(views.LoginView): class SithLoginView(views.LoginView):
""" """
The login View The login View
@ -124,9 +127,10 @@ def password_root_change(request, user_id):
) )
@method_decorator(check_honeypot, name="post")
class SithPasswordResetView(views.PasswordResetView): class SithPasswordResetView(views.PasswordResetView):
""" """
Allows someone to enter an email adresse for resetting password Allows someone to enter an email address for resetting password
""" """
template_name = "core/password_reset.jinja" template_name = "core/password_reset.jinja"
@ -153,12 +157,13 @@ class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
class SithPasswordResetCompleteView(views.PasswordResetCompleteView): class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
""" """
Confirm the password has sucessfully been reset Confirm the password has successfully been reset
""" """
template_name = "core/password_reset_complete.jinja" template_name = "core/password_reset_complete.jinja"
@check_honeypot
def register(request): def register(request):
context = {} context = {}
if request.method == "POST": if request.method == "POST":

36
poetry.lock generated
View File

@ -426,13 +426,13 @@ files = [
[[package]] [[package]]
name = "django" name = "django"
version = "4.2.13" version = "4.2.14"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"},
{file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"},
] ]
[package.dependencies] [package.dependencies]
@ -477,17 +477,17 @@ test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytes
[[package]] [[package]]
name = "django-debug-toolbar" name = "django-debug-toolbar"
version = "4.3.0" version = "4.4.5"
description = "A configurable set of panels that display various debug information about the current request/response." description = "A configurable set of panels that display various debug information about the current request/response."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"}, {file = "django_debug_toolbar-4.4.5-py3-none-any.whl", hash = "sha256:91425606673ee674d780f7aeedf3595c264eb382dcf41f55c6779577900904c0"},
{file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"}, {file = "django_debug_toolbar-4.4.5.tar.gz", hash = "sha256:8298ce966b4c8fc71430082dd4739ef2badb5f867734e1973a413c4ab2ea81b7"},
] ]
[package.dependencies] [package.dependencies]
django = ">=3.2.4" django = ">=4.2.9"
sqlparse = ">=0.2" sqlparse = ">=0.2"
[[package]] [[package]]
@ -508,6 +508,20 @@ packaging = "*"
elasticsearch = ["elasticsearch (>=5,<8)"] elasticsearch = ["elasticsearch (>=5,<8)"]
testing = ["coverage", "geopy (==2)", "pysolr (>=3.7)", "python-dateutil", "requests", "whoosh (>=2.5.4,<3.0)"] testing = ["coverage", "geopy (==2)", "pysolr (>=3.7)", "python-dateutil", "requests", "whoosh (>=2.5.4,<3.0)"]
[[package]]
name = "django-honeypot"
version = "1.2.0"
description = "Django honeypot field utilities"
optional = false
python-versions = "<4.0,>=3.8"
files = [
{file = "django_honeypot-1.2.0-py3-none-any.whl", hash = "sha256:53dd5f8dd96ef1bb7e31b5514c0dc2caae9577e78ebdf03ca4e0f304a7422aba"},
{file = "django_honeypot-1.2.0.tar.gz", hash = "sha256:25fca02e786aec26649bd13b37a95c846e09ab3cfc10f28db2f7dfaa77b9b9c6"},
]
[package.dependencies]
Django = ">=3.2,<5.1"
[[package]] [[package]]
name = "django-jinja" name = "django-jinja"
version = "2.11.0" version = "2.11.0"
@ -669,13 +683,13 @@ python-dateutil = ">=2.7"
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.36" version = "2.6.0"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
] ]
[package.extras] [package.extras]
@ -1867,4 +1881,4 @@ filelock = ">=3.4"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "8524ed5f593973edf05b3c01010c1f2345b7e799089c3e38274304bdedf8b3cb" content-hash = "51820883f41bdf40f00296b722ebdd9ac386e43ef1424ef990b29bac579ecbab"

View File

@ -46,6 +46,7 @@ django-countries = "^7.5.1"
dict2xml = "^1.7.3" dict2xml = "^1.7.3"
Sphinx = "^5" # Needed for building xapian Sphinx = "^5" # Needed for building xapian
tomli = "^2.0.1" tomli = "^2.0.1"
django-honeypot = "^1.2.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^4.0.0" django-debug-toolbar = "^4.0.0"

View File

@ -1,5 +1,5 @@
# #
# Copyright 2016,2017 # Copyright 2016,2017,2024
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr> # - Sli <antoine@bartuccio.fr>
# #
@ -17,7 +17,7 @@
# details. # details.
# #
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
@ -54,6 +54,10 @@ os.environ["HTTPS"] = "off"
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2" SECRET_KEY = "(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2"
# Those values are to be changed in production to be more effective
HONEYPOT_FIELD_NAME = "body2"
HONEYPOT_VALUE = "content"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
TESTING = "pytest" in sys.modules TESTING = "pytest" in sys.modules
@ -75,6 +79,7 @@ INSTALLED_APPS = (
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.sites", "django.contrib.sites",
"honeypot",
"django_jinja", "django_jinja",
"rest_framework", "rest_framework",
"ajax_select", "ajax_select",
@ -143,6 +148,7 @@ TEMPLATES = [
"django_jinja.builtins.extensions.UrlsExtension", "django_jinja.builtins.extensions.UrlsExtension",
"django_jinja.builtins.extensions.StaticFilesExtension", "django_jinja.builtins.extensions.StaticFilesExtension",
"django_jinja.builtins.extensions.DjangoFiltersExtension", "django_jinja.builtins.extensions.DjangoFiltersExtension",
"core.templatetags.extensions.HoneypotExtension",
], ],
"filters": { "filters": {
"markdown": "core.templatetags.renderer.markdown", "markdown": "core.templatetags.renderer.markdown",