diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 3a236b58512d1010200fe03a6e643579614ad7cc..4083889191653f3b3464621e2b8374370f43dd47 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -246,3 +246,31 @@ CROSSREF_LOGIN_PASSWORD = '' RECAPTCHA_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe' NOCAPTCHA = True + + +# PASSWORDS + +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', +] +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'OPTIONS': { + 'min_length': 8, + } + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] diff --git a/requirements.txt b/requirements.txt index 8a82ef7057d72bccf77dc2a8d9aeec140e2861bf..fe99a6a6fd57b124ee6a3bc066c10aa7087b4092 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ alabaster==0.7.9 +argon2-cffi==16.3.0 Babel==2.3.4 Django==1.10.3 django_ajax_selects==1.5.2 diff --git a/scipost/forms.py b/scipost/forms.py index 8fde48c4e6778911e6bf9b1939a704a763de99b1..21c68fe51c5aa3a94289857a942539a620b7757d 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -1,6 +1,8 @@ from django import forms from django.contrib.auth import authenticate from django.contrib.auth.models import User, Group +from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse_lazy from django.utils.http import is_safe_url @@ -16,7 +18,7 @@ from .models import Contributor, DraftInvitation, RegistrationInvitation,\ UnavailabilityPeriod, PrecookedEmail from journals.models import Publication -from mailing_lists.models import MailchimpList, MailchimpSubscription +# from mailing_lists.models import MailchimpList, MailchimpSubscription REGISTRATION_REFUSAL_CHOICES = ( @@ -60,13 +62,28 @@ class RegistrationForm(forms.Form): required=False) username = forms.CharField(label='* Username', max_length=100) password = forms.CharField(label='* Password', widget=forms.PasswordInput()) - password_verif = forms.CharField(label='* Verify password', widget=forms.PasswordInput()) + password_verif = forms.CharField(label='* Verify password', widget=forms.PasswordInput(), + help_text='Your password must contain at least 8 characters') captcha = ReCaptchaField(attrs={'theme': 'clean'}, label='*Please verify to continue:') + def clean_password(self): + password = self.cleaned_data.get('password', '') + user = User( + username=self.cleaned_data.get('username', ''), + first_name=self.cleaned_data.get('first_name', ''), + last_name=self.cleaned_data.get('last_name', ''), + email=self.cleaned_data.get('email', '') + ) + try: + validate_password(password, user) + except ValidationError as error_message: + self.add_error('password', error_message) + return password + def clean_password_verif(self): - if self.cleaned_data['password'] != self.cleaned_data['password_verif']: - self.add_error('password', 'Your passwords must match') - self.add_error('password_verif', 'Your passwords must match') + if self.cleaned_data.get('password', '') != self.cleaned_data.get('password_verif', ''): + self.add_error('password_verif', 'Your password entries must match') + return self.cleaned_data.get('password_verif', '') def clean_username(self): if User.objects.filter(username=self.cleaned_data['username']).exists(): @@ -245,6 +262,40 @@ class PasswordChangeForm(forms.Form): password_new = forms.CharField(label='New password', widget=forms.PasswordInput()) password_verif = forms.CharField(label='Reenter new password', widget=forms.PasswordInput()) + def __init__(self, *args, **kwargs): + self.current_user = kwargs.pop('current_user', None) + super().__init__(*args, **kwargs) + + def clean_password_prev(self): + '''Check if old password is correct.''' + password_prev = self.cleaned_data['password_prev'] + if not self.current_user.check_password(password_prev): + self.add_error('password_prev', + 'The currently existing password you entered is incorrect') + return password_prev + + def clean_password_new(self): + '''Validate the newly chosen password using the validators as per the settingsfile.''' + password = self.cleaned_data['password_new'] + try: + validate_password(password, self.current_user) + except ValidationError as error_message: + self.add_error('password_new', error_message) + return password + + def clean_password_verif(self): + '''Check if the new password's match to ensure the user entered new password correctly.''' + password_verif = self.cleaned_data.get('password_verif', '') + if self.cleaned_data['password_new'] != password_verif: + self.add_error('password_verif', 'Your new password entries must match') + return password_verif + + def save_new_password(self): + '''Save new password is form is valid.''' + if not self.errors: + self.current_user.set_password(self.cleaned_data['password_new']) + self.current_user.save() + AUTHORSHIP_CLAIM_CHOICES = ( ('-', '-'), diff --git a/scipost/templates/scipost/change_password.html b/scipost/templates/scipost/change_password.html index c0e94de5a9504fcd7bc707c9e250539acf6bfbab..ffac2c40459b6a3d8c61fa8ddc8f8d6f517494aa 100644 --- a/scipost/templates/scipost/change_password.html +++ b/scipost/templates/scipost/change_password.html @@ -6,26 +6,16 @@ {% block content %} -{% if ack %} - <div class="row"> - <div class="col-12"> - <h1>Your SciPost password has been successfully changed</h1> - </div> - </div> -{% else %} - <div class="row"> - <div class="col-lg-8 offset-lg-2"> - <h1 class="highlight">Change your SciPost password</h1> - {% if errormessage %} - <p class="text-danger">{{ errormessage }}</p> - {% endif %} - <form action="{% url 'scipost:change_password' %}" method="post"> - {% csrf_token %} - {{form|bootstrap}} - <input type="submit" class="btn btn-secondary" value="Change" /> - </form> - </div> +<div class="row"> + <div class="col-lg-8 offset-lg-2"> + <h1 class="highlight">Change your SciPost password</h1> + + <form action="{% url 'scipost:change_password' %}" method="post"> + {% csrf_token %} + {{form|bootstrap}} + <input type="submit" class="btn btn-secondary" value="Change" /> + </form> </div> -{% endif %} +</div> {% endblock content %} diff --git a/scipost/templates/scipost/reset_password.html b/scipost/templates/scipost/reset_password.html index 429ed46c71c7a1f728fdb45bd427a4b18bc62ffd..e8169dba2c0608cf7b3b4d23b8c83c65d12f15f8 100644 --- a/scipost/templates/scipost/reset_password.html +++ b/scipost/templates/scipost/reset_password.html @@ -2,20 +2,20 @@ {% block pagetitle %}: Reset Password{% endblock pagetitle %} -{% block bodysup %} - {% load bootstrap %} -<div class="container"> - <div class="row"> - <div class="col-md-4"> - <h3>Reset password request form</h3> - <form method="post"> - {% csrf_token %} - {{ form|bootstrap }} - <input class="btn btn-primary" type="submit" value="Submit" /> - </form> +{% block content %} + + +<div class="row"> + <div class="col-md-4 offset-md-4"> + <h3>Reset password request form</h3> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit" /> + </form> </div> - </div> </div> -{% endblock bodysup %} + +{% endblock %} diff --git a/scipost/templates/scipost/reset_password_complete.html b/scipost/templates/scipost/reset_password_complete.html index f675ab9967a7677319b1e252b57c52086aef1a33..dd226a8df6f40d4867d08a24b794f52fc3889a0a 100644 --- a/scipost/templates/scipost/reset_password_complete.html +++ b/scipost/templates/scipost/reset_password_complete.html @@ -3,5 +3,5 @@ {% block pagetitle %}: Reset password complete{% endblock pagetitle %} {% block bodysup %} -<p>You have successfully reset your password.</p> + <p>You have successfully reset your password.</p> {% endblock bodysup %} diff --git a/scipost/templates/scipost/reset_password_confirm.html b/scipost/templates/scipost/reset_password_confirm.html index e302efffc0b6085d18bfd325611f20a61554f470..60afebc99f0f143ac49d577665f4475f7e3a6cb2 100644 --- a/scipost/templates/scipost/reset_password_confirm.html +++ b/scipost/templates/scipost/reset_password_confirm.html @@ -2,16 +2,22 @@ {% block pagetitle %}: Reset password confirm{% endblock pagetitle %} -{% block bodysup %} -<section> +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-md-6 offset-md-3"> {% if validlink %} <form method="post"> {% csrf_token %} - {{ form.as_p }} - <button type="submit">Submit</button> + {{ form|bootstrap }} + <input class="btn btn-secondary" type="submit" value="Submit"> </form> {% else %} <p>This reset link is no longer valid!</p> {% endif %} -</section> -{% endblock bodysup %} + </div> +</div> + +{% endblock content %} diff --git a/scipost/views.py b/scipost/views.py index c113d3b9f496c6007a23855ec3f571200d329668..d603e50197e61cdef8f67dfad460d6dc378915e1 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -3,7 +3,7 @@ import re from django.utils import timezone from django.shortcuts import get_object_or_404, render from django.contrib import messages -from django.contrib.auth import login, logout +from django.contrib.auth import login, logout, update_session_auth_hash from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.contrib.auth.views import password_reset, password_reset_confirm @@ -914,23 +914,14 @@ def personal_page(request): @login_required def change_password(request): - form = PasswordChangeForm(request.POST or None) - ack = False + form = PasswordChangeForm(request.POST or None, current_user=request.user) if form.is_valid(): - if not request.user.check_password(form.cleaned_data['password_prev']): - return render( - request, 'scipost/change_password.html', - {'form': form, - 'errormessage': 'The currently existing password you entered is incorrect'}) - if form.cleaned_data['password_new'] != form.cleaned_data['password_verif']: - return render(request, 'scipost/change_password.html', { - 'form': form, - 'errormessage': 'Your new password entries must match'}) - request.user.set_password(form.cleaned_data['password_new']) - request.user.save() - ack = True - - return render(request, 'scipost/change_password.html', {'ack': ack, 'form': form}) + form.save_new_password() + # Update user's session hash to stay logged in. + update_session_auth_hash(request, request.user) + messages.success(request, 'Your SciPost password has been successfully changed') + return redirect(reverse('scipost:personal_page')) + return render(request, 'scipost/change_password.html', {'form': form}) def reset_password_confirm(request, uidb64=None, token=None):