stages/email: add form for sending email to prevent spam

stages/email: make token validity configurable
This commit is contained in:
Jens Langhammer 2020-05-10 20:50:27 +02:00
parent 206cf4967d
commit d4f149bc02
8 changed files with 65 additions and 30 deletions

View File

@ -26,7 +26,7 @@ class AuthenticationStage(TemplateView):
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]: def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
kwargs["config"] = CONFIG.y("passbook") kwargs["config"] = CONFIG.y("passbook")
kwargs["title"] = _("Log in to your account") kwargs["title"] = self.executor.flow.name
kwargs["primary_action"] = _("Log in") kwargs["primary_action"] = _("Log in")
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context: if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
kwargs["user"] = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] kwargs["user"] = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]

View File

@ -22,8 +22,6 @@ class EmailStageSerializer(ModelSerializer):
"use_ssl", "use_ssl",
"timeout", "timeout",
"from_address", "from_address",
"ssl_keyfile",
"ssl_certfile",
] ]
extra_kwargs = {"password": {"write_only": True}} extra_kwargs = {"password": {"write_only": True}}

View File

@ -5,6 +5,12 @@ from django.utils.translation import gettext_lazy as _
from passbook.stages.email.models import EmailStage from passbook.stages.email.models import EmailStage
class EmailStageSendForm(forms.Form):
"""Form used when sending the e-mail to prevent multiple emails being sent"""
invalid = forms.CharField(widget=forms.HiddenInput, required=True)
class EmailStageForm(forms.ModelForm): class EmailStageForm(forms.ModelForm):
"""Form to create/edit Dummy Stage""" """Form to create/edit Dummy Stage"""
@ -21,20 +27,14 @@ class EmailStageForm(forms.ModelForm):
"use_ssl", "use_ssl",
"timeout", "timeout",
"from_address", "from_address",
"ssl_keyfile",
"ssl_certfile",
] ]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
"host": forms.TextInput(), "host": forms.TextInput(),
"username": forms.TextInput(), "username": forms.TextInput(),
"password": forms.TextInput(), "password": forms.TextInput(),
"ssl_keyfile": forms.TextInput(),
"ssl_certfile": forms.TextInput(),
} }
labels = { labels = {
"use_tls": _("Use TLS"), "use_tls": _("Use TLS"),
"use_ssl": _("Use SSL"), "use_ssl": _("Use SSL"),
"ssl_keyfile": _("SSL Keyfile (optional)"),
"ssl_certfile": _("SSL Certfile (optional)"),
} }

View File

@ -0,0 +1,22 @@
# Generated by Django 3.0.5 on 2020-05-10 18:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_stages_email", "0001_initial"),
]
operations = [
migrations.RemoveField(model_name="emailstage", name="ssl_certfile",),
migrations.RemoveField(model_name="emailstage", name="ssl_keyfile",),
migrations.AddField(
model_name="emailstage",
name="token_expiry",
field=models.IntegerField(
default=30, help_text="Time in minutes the token sent is valid."
),
),
]

View File

@ -17,8 +17,9 @@ class EmailStage(Stage):
use_ssl = models.BooleanField(default=False) use_ssl = models.BooleanField(default=False)
timeout = models.IntegerField(default=10) timeout = models.IntegerField(default=10)
ssl_keyfile = models.TextField(default=None, blank=True, null=True) token_expiry = models.IntegerField(
ssl_certfile = models.TextField(default=None, blank=True, null=True) default=30, help_text=_("Time in minutes the token sent is valid.")
)
from_address = models.EmailField(default="system@passbook.local") from_address = models.EmailField(default="system@passbook.local")
@ -36,8 +37,6 @@ class EmailStage(Stage):
use_tls=self.use_tls, use_tls=self.use_tls,
use_ssl=self.use_ssl, use_ssl=self.use_ssl,
timeout=self.timeout, timeout=self.timeout,
ssl_certfile=self.ssl_certfile,
ssl_keyfile=self.ssl_keyfile,
) )
def __str__(self): def __str__(self):

View File

@ -1,26 +1,28 @@
"""passbook multi-stage authentication engine""" """passbook multi-stage authentication engine"""
from datetime import timedelta from datetime import timedelta
from django.contrib import messages from django.http import HttpResponse
from django.http import HttpRequest
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Nonce from passbook.core.models import Nonce
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.stage import AuthenticationStage from passbook.flows.stage import AuthenticationStage
from passbook.stages.email.forms import EmailStageSendForm
from passbook.stages.email.tasks import send_mails from passbook.stages.email.tasks import send_mails
from passbook.stages.email.utils import TemplateEmailMessage from passbook.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger() LOGGER = get_logger()
class EmailStageView(AuthenticationStage): class EmailStageView(FormView, AuthenticationStage):
"""E-Mail stage which sends E-Mail for verification""" """E-Mail stage which sends E-Mail for verification"""
form_class = EmailStageSendForm
template_name = "stages/email/waiting_message.html" template_name = "stages/email/waiting_message.html"
def get_full_url(self, **kwargs) -> str: def get_full_url(self, **kwargs) -> str:
@ -32,13 +34,11 @@ class EmailStageView(AuthenticationStage):
relative_url = f"{base_url}?{urlencode(kwargs)}" relative_url = f"{base_url}?{urlencode(kwargs)}"
return self.request.build_absolute_uri(relative_url) return self.request.build_absolute_uri(relative_url)
def get(self, request, *args, **kwargs): def form_invalid(self, form: EmailStageSendForm) -> HttpResponse:
# TODO: Form to make sure email is only sent once
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
# TODO: Get expiry from Stage setting
valid_delta = timedelta( valid_delta = timedelta(
minutes=31 minutes=self.executor.current_stage.token_expiry + 1
) # 31 because django timesince always rounds down ) # + 1 because django timesince always rounds down
nonce = Nonce.objects.create(user=pending_user, expires=now() + valid_delta) nonce = Nonce.objects.create(user=pending_user, expires=now() + valid_delta)
# Send mail to user # Send mail to user
message = TemplateEmailMessage( message = TemplateEmailMessage(
@ -52,12 +52,11 @@ class EmailStageView(AuthenticationStage):
}, },
) )
send_mails(self.executor.current_stage, message) send_mails(self.executor.current_stage, message)
messages.success(request, _("Check your E-Mails for a password reset link."))
# We can't call stage_ok yet, as we're still waiting # We can't call stage_ok yet, as we're still waiting
# for the user to click the link in the email # for the user to click the link in the email
# return self.executor.stage_ok() # return self.executor.stage_ok()
return super().get(request, *args, **kwargs) return super().form_invalid(form)
def post(self, request: HttpRequest): # def post(self, request: HttpRequest):
"""Just redirect to next stage""" # """Just redirect to next stage"""
return self.executor.stage_ok() # return self.executor.()

View File

@ -1 +1,21 @@
check your emails mate {% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% block card %}
<form method="POST" class="pf-c-form">
<p>
{% blocktrans %}
Check your E-Mails for a password reset link.
{% endblocktrans %}
</p>
{% csrf_token %}
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Recovery E-Mail." %}</button>
</div>
</form>
{% endblock %}

View File

@ -1,7 +1,6 @@
"""email utils""" """email utils"""
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
class TemplateEmailMessage(EmailMultiAlternatives): class TemplateEmailMessage(EmailMultiAlternatives):
@ -9,8 +8,6 @@ class TemplateEmailMessage(EmailMultiAlternatives):
def __init__(self, template_name=None, template_context=None, **kwargs): def __init__(self, template_name=None, template_context=None, **kwargs):
html_content = render_to_string(template_name, template_context) html_content = render_to_string(template_name, template_context)
if "body" not in kwargs:
kwargs["body"] = strip_tags(html_content)
super().__init__(**kwargs) super().__init__(**kwargs)
self.content_subtype = "html" self.content_subtype = "html"
self.attach_alternative(html_content, "text/html") self.attach_alternative(html_content, "text/html")