improve one time login security, remove utils

This commit is contained in:
Tom Irgang 2023-04-25 14:17:59 +02:00
parent 43dbd24dd4
commit cdbc26834c
5 changed files with 195 additions and 185 deletions

View file

@ -1,11 +1,36 @@
"""
Authentication backends for RepApp.
"""
import unicodedata
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from .utils import generate_username, create_repapp_user
from .models import OneTimeLogin
from .models import OneTimeLogin, Organisator, Reparateur
def generate_username(email):
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
return unicodedata.normalize('NFKC', email)[:150]
def create_repapp_user(user):
organisator = Organisator.objects.filter(mail=user.email).first()
if not organisator:
reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur:
reparateur = Reparateur(
name=user.username,
mail=user.email,
)
reparateur.save()
else:
reparateur.name = user.username
reparateur.save()
else:
organisator.name = user.username
organisator.save()
class OneTimeLoginBackend(ModelBackend):

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-04-25 12:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0003_guest_user'),
]
operations = [
migrations.AlterField(
model_name='onetimelogin',
name='secret',
field=models.CharField(max_length=200, unique=True, verbose_name='Geheimnis'),
),
]

View file

@ -5,7 +5,13 @@ import django.utils.timezone
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractUser
from .utils import device_directory_path
def device_directory_path(instance, filename):
"""
device_directory_path generates a device-specific storage path for file uploads.
"""
return f'device_{instance.identifier}/{filename}'
class CustomUser(AbstractUser):

View file

@ -1,179 +0,0 @@
import os
import unicodedata
import datetime
import random
from hashlib import sha256
from django.template.loader import render_to_string
from django.urls import reverse
from django.core.mail import send_mail
from django.contrib import messages
from repapp.models import Organisator, Reparateur, OneTimeLogin
def create_one_time_login(user, url) -> str:
"""
create_one_time_login create a one time login object for user logins.
"""
secret = sha256(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
'utf-8')
).hexdigest()
hash = sha256(secret.encode('utf-8')).hexdigest()
otl = OneTimeLogin(
secret=hash,
user=user,
url=url,
)
otl.save()
return secret
def send_one_time_login_mail(secret, mail, request):
url = request.build_absolute_uri(
f'/onetimelogin/{secret}/')
subject = render_to_string(
'repapp/mail/mail_one_time_login_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_one_time_login_text.html', {
'link': url,
})
html = render_to_string('repapp/mail/mail_one_time_login_html.html', {
'link': url,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[mail],
fail_silently=True,
html_message=html
)
if ok > 0:
messages.add_message(request, messages.INFO,
'Sie haben einen neuen Login per eMail erhalten.')
def create_repapp_user(user):
organisator = Organisator.objects.filter(mail=user.email).first()
if not organisator:
reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur:
reparateur = Reparateur(
name=user.username,
mail=user.email,
)
reparateur.save()
else:
reparateur.name = user.username
reparateur.save()
else:
organisator.name = user.username
organisator.save()
def generate_username(email):
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
return unicodedata.normalize('NFKC', email)[:150]
def send_confirmation_mails(device, guest, cafe, request):
organizers = []
for organizer in Organisator.objects.all():
organizers.append(organizer.mail)
text = render_to_string('repapp/mail/notice_new_device.html', {
'guest': device.guest,
'device': device,
'cafe': device.cafe,
})
send_mail(
subject=f"Neues Gerät { device.device }",
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=organizers,
fail_silently=True
)
path = reverse('view_device', kwargs={
'device_identifier': device.identifier})
url = request.build_absolute_uri(path)
secret = create_one_time_login(guest.user, url)
login_url = request.build_absolute_uri(f'/onetimelogin/{secret}/')
subject = render_to_string('repapp/mail/mail_register_device_subject.html', {
'cafe': cafe,
}).replace('\n', '')
text = render_to_string('repapp/mail/mail_register_device_text.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
html = render_to_string('repapp/mail/mail_register_device_html.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
mail_count = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.mail}"],
fail_silently=True,
html_message=html
)
if mail_count > 0:
device.confirmed = True
device.save()
else:
messages.add_message(request, messages.ERROR,
'Fehler beim senden der Bestätigungs-eMail!')
def send_guest_account_mail(guest, password, request):
url = request.build_absolute_uri('/guest/profile/')
subject = render_to_string(
'repapp/mail/mail_new_guest_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_new_guest_text.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
html = render_to_string('repapp/mail/mail_new_guest_html.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.user.email}"],
fail_silently=True,
html_message=html
)
if ok > 0:
messages.add_message(request, messages.INFO,
'Sie haben ihre Benutzerdaten per eMail erhalten.')
def is_member(user):
organisator = Organisator.objects.filter(mail=user.email).first()
reparateur = Reparateur.objects.filter(mail=user.email).first()
return organisator or reparateur
def device_directory_path(instance, filename):
"""
device_directory_path generates a device-specific storage path for file uploads.
"""
return f'device_{instance.identifier}/{filename}'

View file

@ -5,7 +5,12 @@ import datetime
import random
import string
import time
import os
from hashlib import sha256
from django.template.loader import render_to_string
from django.urls import reverse
from django.core.mail import send_mail
from django.contrib import messages
from django.views import generic
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, render
@ -15,11 +20,146 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate, login
from django.utils.timezone import now
from repapp.models import CustomUser
from repapp.models import CustomUser, Organisator, Reparateur
from .models import Cafe, Device, Guest, OneTimeLogin
from .forms import RegisterDevice, RegisterGuest
from .utils import (send_confirmation_mails, create_one_time_login,
send_one_time_login_mail, send_guest_account_mail, is_member)
def send_one_time_login_mail(secret, mail, request):
url = request.build_absolute_uri(
f'/onetimelogin/{secret}/')
subject = render_to_string(
'repapp/mail/mail_one_time_login_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_one_time_login_text.html', {
'link': url,
})
html = render_to_string('repapp/mail/mail_one_time_login_html.html', {
'link': url,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[mail],
fail_silently=True,
html_message=html
)
if ok > 0:
messages.add_message(request, messages.INFO,
'Sie haben einen neuen Login per eMail erhalten.')
def send_confirmation_mails(device, guest, cafe, request):
organizers = []
for organizer in Organisator.objects.all():
organizers.append(organizer.mail)
text = render_to_string('repapp/mail/notice_new_device.html', {
'guest': device.guest,
'device': device,
'cafe': device.cafe,
})
send_mail(
subject=f"Neues Gerät { device.device }",
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=organizers,
fail_silently=True
)
path = reverse('view_device', kwargs={
'device_identifier': device.identifier})
url = request.build_absolute_uri(path)
secret = create_one_time_login(guest.user, url)
login_url = request.build_absolute_uri(f'/onetimelogin/{secret}/')
subject = render_to_string('repapp/mail/mail_register_device_subject.html', {
'cafe': cafe,
}).replace('\n', '')
text = render_to_string('repapp/mail/mail_register_device_text.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
html = render_to_string('repapp/mail/mail_register_device_html.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
mail_count = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.mail}"],
fail_silently=True,
html_message=html
)
if mail_count > 0:
device.confirmed = True
device.save()
else:
messages.add_message(request, messages.ERROR,
'Fehler beim senden der Bestätigungs-eMail!')
def send_guest_account_mail(guest, password, request):
url = request.build_absolute_uri('/guest/profile/')
subject = render_to_string(
'repapp/mail/mail_new_guest_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_new_guest_text.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
html = render_to_string('repapp/mail/mail_new_guest_html.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.user.email}"],
fail_silently=True,
html_message=html
)
if ok > 0:
messages.add_message(request, messages.INFO,
'Sie haben ihre Benutzerdaten per eMail erhalten.')
def is_member(user):
organisator = Organisator.objects.filter(mail=user.email).first()
reparateur = Reparateur.objects.filter(mail=user.email).first()
return organisator or reparateur
def create_one_time_login(user, url) -> str:
"""
create_one_time_login create a one time login object for user logins.
"""
secret = sha256(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
'utf-8')
).hexdigest()
hash = sha256(secret.encode('utf-8')).hexdigest()
otl = OneTimeLogin(
secret=hash,
user=user,
url=url,
)
otl.save()
return secret
class IndexView(generic.ListView):