From 2206ef352e58aa0e57819814874233750e5daf7d Mon Sep 17 00:00:00 2001 From: Frederic Date: Sun, 28 Apr 2019 13:07:11 +0200 Subject: [PATCH 1/4] implement password reset via hashed email address --- bam/admin.py | 16 +++++++++++++++- bam/forms.py | 13 +++++++++++++ bam/migrations/0001_initial.py | 25 +++++++++++++++++++++++++ bam/models.py | 9 ++++++++- bam/urls.py | 8 +++++++- bam/views.py | 1 + 6 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 bam/forms.py create mode 100644 bam/migrations/0001_initial.py diff --git a/bam/admin.py b/bam/admin.py index 8c38f3f..a734a51 100644 --- a/bam/admin.py +++ b/bam/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User +from .models import Account -# Register your models here. + +class AccountInline(admin.StackedInline): + model = Account + can_delete = False + + +class UserAdmin(BaseUserAdmin): + inlines = (AccountInline,) + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) diff --git a/bam/forms.py b/bam/forms.py new file mode 100644 index 0000000..c752de4 --- /dev/null +++ b/bam/forms.py @@ -0,0 +1,13 @@ +import hashlib +from django.contrib.auth.forms import PasswordResetForm +from bam.models import Account + + +class HashedEmailPasswordResetForm(PasswordResetForm): + def get_users(self, email): + hashed_email = hashlib.sha256(bytes(email, 'utf-8')).hexdigest() + accounts = Account.objects.filter(hashed_email=hashed_email) + if accounts.count() > 0: + return (a.user for a in accounts if a.user.has_usable_password()) + else: + return super().get_users(email) diff --git a/bam/migrations/0001_initial.py b/bam/migrations/0001_initial.py new file mode 100644 index 0000000..d32bbec --- /dev/null +++ b/bam/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2 on 2019-04-28 10:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hashed_email', models.CharField(max_length=128)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/bam/models.py b/bam/models.py index 71a8362..8cff54b 100644 --- a/bam/models.py +++ b/bam/models.py @@ -1,3 +1,10 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. + +class Account(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + hashed_email = models.CharField(max_length=128) + + def __str__(self): + return '%s' % (self.user.username) diff --git a/bam/urls.py b/bam/urls.py index c7d8c5d..16f36e1 100644 --- a/bam/urls.py +++ b/bam/urls.py @@ -3,6 +3,7 @@ from django.urls import path from django.urls import include from django.views.generic.base import RedirectView from bam.views import ProfileView +from bam.forms import HashedEmailPasswordResetForm import django.contrib.auth.views as auth_views urlpatterns = [ @@ -28,7 +29,8 @@ urlpatterns = [ name='password_change_done'), path('password_reset/', auth_views.PasswordResetView.as_view( - template_name='bam/password_reset.html' + template_name='bam/password_reset.html', + form_class=HashedEmailPasswordResetForm ), name='password_reset'), path('password_reset_done/', @@ -51,4 +53,8 @@ urlpatterns = [ template_name='bam/password_reset_complete.html' ), name='password_reset_complete'), + + #path('password_reset_hashed/', + # PasswordResetHashedView.as_view(), + # name='password_reset_hashed'), ] diff --git a/bam/views.py b/bam/views.py index 5a9f665..27a42da 100644 --- a/bam/views.py +++ b/bam/views.py @@ -1,5 +1,6 @@ from django.views.generic.base import TemplateView from django.contrib.auth.mixins import LoginRequiredMixin + class ProfileView(LoginRequiredMixin, TemplateView): template_name = 'bam/profile.html' From 529df81239ff517cfb38809abad50cccef708449 Mon Sep 17 00:00:00 2001 From: Frederic Date: Sun, 28 Apr 2019 23:49:22 +0200 Subject: [PATCH 2/4] add separate field to set hashed email address --- bam/admin.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bam/admin.py b/bam/admin.py index a734a51..cd01e0b 100644 --- a/bam/admin.py +++ b/bam/admin.py @@ -1,12 +1,22 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User +from django import forms from .models import Account +class AccountInlineForm(forms.ModelForm): + hased_email_set = forms.EmailField(label='Set hashed email address') + + class Meta: + model = Account + fields = ['hashed_email'] + + class AccountInline(admin.StackedInline): model = Account can_delete = False + form = AccountInlineForm class UserAdmin(BaseUserAdmin): From b84d4ba36d5a6f29b8b8f72ad44d0410162add05 Mon Sep 17 00:00:00 2001 From: Frederic Date: Sun, 28 Apr 2019 23:50:14 +0200 Subject: [PATCH 3/4] add username field, prepare for salted email hash --- bam/forms.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bam/forms.py b/bam/forms.py index c752de4..6a397e2 100644 --- a/bam/forms.py +++ b/bam/forms.py @@ -1,13 +1,18 @@ import hashlib from django.contrib.auth.forms import PasswordResetForm -from bam.models import Account +from django.contrib.auth.hashers import check_password +from django import forms +from django.utils.translation import gettext, gettext_lazy as _ +from .models import Account class HashedEmailPasswordResetForm(PasswordResetForm): + username = forms.CharField(label=_('Username'), max_length=254) + def get_users(self, email): - hashed_email = hashlib.sha256(bytes(email, 'utf-8')).hexdigest() - accounts = Account.objects.filter(hashed_email=hashed_email) - if accounts.count() > 0: - return (a.user for a in accounts if a.user.has_usable_password()) - else: - return super().get_users(email) + accounts = Account.objects.filter( + user__username=self.cleaned_data['username'] + ) + return (a.user for a in accounts if a.user.has_usable_password() and + (check_password(email, a.hashed_email) + or a.user.email == email)) From 4e1a21ee8157ce233bc951301ec8c668c5484c04 Mon Sep 17 00:00:00 2001 From: Frederic Date: Mon, 29 Apr 2019 00:54:57 +0200 Subject: [PATCH 4/4] add setter for hashed password, use it in admin --- bam/admin.py | 9 ++++++++- bam/models.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bam/admin.py b/bam/admin.py index cd01e0b..5d7d4b2 100644 --- a/bam/admin.py +++ b/bam/admin.py @@ -1,22 +1,28 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User +from django.contrib.auth.hashers import make_password from django import forms from .models import Account class AccountInlineForm(forms.ModelForm): - hased_email_set = forms.EmailField(label='Set hashed email address') + hashed_email_set = forms.EmailField(label='Set hashed email address') class Meta: model = Account fields = ['hashed_email'] + def save(self, commit): + self.instance.set_hashed_email(self.cleaned_data['hashed_email_set']) + return super().save(commit) + class AccountInline(admin.StackedInline): model = Account can_delete = False form = AccountInlineForm + readonly_fields = ['hashed_email'] class UserAdmin(BaseUserAdmin): @@ -25,3 +31,4 @@ class UserAdmin(BaseUserAdmin): admin.site.unregister(User) admin.site.register(User, UserAdmin) +admin.site.register(Account) diff --git a/bam/models.py b/bam/models.py index 8cff54b..2f2fe82 100644 --- a/bam/models.py +++ b/bam/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth.models import User +from django.contrib.auth.hashers import make_password class Account(models.Model): @@ -8,3 +9,6 @@ class Account(models.Model): def __str__(self): return '%s' % (self.user.username) + + def set_hashed_email(self, email): + self.hashed_email = make_password(email)