snapshot: preserve totp account security baseline

This commit is contained in:
Md Bayazid Bostame
2026-03-27 02:46:40 +01:00
parent 358a71230d
commit c679488437
18 changed files with 1723 additions and 786 deletions

View File

@@ -3,6 +3,7 @@ from pathlib import Path
from datetime import timedelta
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, PasswordResetForm, SetPasswordForm
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.translation import get_language, gettext as _, gettext_lazy
@@ -10,6 +11,7 @@ from .branding import get_company_email_domain
from .form_builder import apply_form_field_config
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, PortalTrialConfig, UserProfile, WorkflowConfig
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role
from .totp import normalize_totp_token, verify_totp_token
YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')]
@@ -103,6 +105,38 @@ SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'
class AppAuthenticationForm(AuthenticationForm):
username = forms.CharField(label=gettext_lazy('Benutzername'))
password = forms.CharField(label=gettext_lazy('Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}))
otp_code = forms.CharField(
label=gettext_lazy('TOTP-Code'),
required=False,
max_length=12,
widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}),
)
error_messages = {
**AuthenticationForm.error_messages,
'invalid_otp': gettext_lazy('Der TOTP-Code ist ungültig.'),
'missing_otp': gettext_lazy('Bitte geben Sie Ihren TOTP-Code ein.'),
}
def clean(self):
cleaned_data = super().clean()
user = self.get_user()
if not user:
return cleaned_data
profile, _ = UserProfile.objects.get_or_create(user=user)
if profile.totp_enabled:
otp_code = normalize_totp_token(cleaned_data.get('otp_code'))
if not otp_code:
raise ValidationError(
self.error_messages['missing_otp'],
code='missing_otp',
)
if not profile.totp_secret or not verify_totp_token(profile.totp_secret, otp_code, for_time=int(timezone.now().timestamp())):
raise ValidationError(
self.error_messages['invalid_otp'],
code='invalid_otp',
)
return cleaned_data
class AppPasswordResetForm(PasswordResetForm):
@@ -221,6 +255,71 @@ class AccountDetailsForm(forms.Form):
return self.user, self.profile
class AccountTOTPEnableForm(forms.Form):
current_password = forms.CharField(
label=gettext_lazy('Aktuelles Passwort'),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
)
verification_code = forms.CharField(
label=gettext_lazy('TOTP-Code'),
max_length=12,
widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}),
)
def __init__(self, *args, user=None, secret: str = '', **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.secret = secret
def clean_current_password(self):
password = self.cleaned_data.get('current_password') or ''
if not self.user or not self.user.check_password(password):
raise ValidationError(_('Das aktuelle Passwort ist nicht korrekt.'))
return password
def clean_verification_code(self):
code = normalize_totp_token(self.cleaned_data.get('verification_code'))
if not code:
raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code ein.'))
if not self.secret or not verify_totp_token(self.secret, code, for_time=int(timezone.now().timestamp())):
raise ValidationError(_('Der TOTP-Code ist ungültig.'))
return code
class AccountTOTPDisableForm(forms.Form):
current_password = forms.CharField(
label=gettext_lazy('Aktuelles Passwort'),
strip=False,
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
)
verification_code = forms.CharField(
label=gettext_lazy('TOTP-Code'),
max_length=12,
widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}),
)
def __init__(self, *args, user=None, profile=None, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
self.profile = profile
def clean_current_password(self):
password = self.cleaned_data.get('current_password') or ''
if not self.user or not self.user.check_password(password):
raise ValidationError(_('Das aktuelle Passwort ist nicht korrekt.'))
return password
def clean_verification_code(self):
code = normalize_totp_token(self.cleaned_data.get('verification_code'))
if not code:
raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code ein.'))
secret = getattr(self.profile, 'totp_secret', '') or ''
if not secret or not verify_totp_token(secret, code, for_time=int(timezone.now().timestamp())):
raise ValidationError(_('Der TOTP-Code ist ungültig.'))
return code
class UserManagementCreateForm(forms.Form):
first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False)
last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False)