improve one time login security, remove utils
This commit is contained in:
parent
43dbd24dd4
commit
cdbc26834c
5 changed files with 195 additions and 185 deletions
|
|
@ -1,11 +1,36 @@
|
||||||
"""
|
"""
|
||||||
Authentication backends for RepApp.
|
Authentication backends for RepApp.
|
||||||
"""
|
"""
|
||||||
|
import unicodedata
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
|
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
|
||||||
from .utils import generate_username, create_repapp_user
|
from .models import OneTimeLogin, Organisator, Reparateur
|
||||||
from .models import OneTimeLogin
|
|
||||||
|
|
||||||
|
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):
|
class OneTimeLoginBackend(ModelBackend):
|
||||||
|
|
|
||||||
18
rc_hip/repapp/migrations/0004_alter_onetimelogin_secret.py
Normal file
18
rc_hip/repapp/migrations/0004_alter_onetimelogin_secret.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,13 @@ import django.utils.timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import AbstractUser
|
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):
|
class CustomUser(AbstractUser):
|
||||||
|
|
|
||||||
|
|
@ -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}'
|
|
||||||
|
|
@ -5,7 +5,12 @@ import datetime
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
from hashlib import sha256
|
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.views import generic
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import get_object_or_404, render
|
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.decorators import login_required
|
||||||
from django.contrib.auth import authenticate, login
|
from django.contrib.auth import authenticate, login
|
||||||
from django.utils.timezone import now
|
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 .models import Cafe, Device, Guest, OneTimeLogin
|
||||||
from .forms import RegisterDevice, RegisterGuest
|
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):
|
class IndexView(generic.ListView):
|
||||||
|
|
|
||||||
Reference in a new issue