Initial mfa support (for internal users) #93591
@ -287,6 +287,7 @@ def construct_password_changed(user):
|
||||
def construct_mfa_new_device(user, device_type):
|
||||
context = {
|
||||
"device_type": device_type,
|
||||
"support_email": settings.SUPPORT_EMAIL,
|
||||
"user": user,
|
||||
}
|
||||
email_body_txt = loader.render_to_string(
|
||||
@ -311,6 +312,7 @@ def construct_mfa_disabled(user):
|
||||
|
||||
def construct_mfa_recovery_used(user):
|
||||
context = {
|
||||
"support_email": settings.SUPPORT_EMAIL,
|
||||
"user": user,
|
||||
}
|
||||
email_body_txt = loader.render_to_string(
|
||||
|
@ -316,7 +316,7 @@ class PasswordChangeForm(BootstrapModelFormMixin, auth_forms.PasswordChangeForm)
|
||||
def save(self, *args, **kwargs):
|
||||
user = super().save(*args, **kwargs)
|
||||
if user.has_confirmed_email:
|
||||
bid_main.tasks.send_password_changed_email(user_pk=user.pk)
|
||||
bid_main.tasks.send_mail_password_changed(user_pk=user.pk)
|
||||
return user
|
||||
|
||||
|
||||
@ -335,7 +335,7 @@ class SetPasswordForm(auth_forms.SetPasswordForm):
|
||||
def save(self, *args, **kwargs):
|
||||
user = super().save(*args, **kwargs)
|
||||
if user.has_confirmed_email:
|
||||
bid_main.tasks.send_password_changed_email(user_pk=user.pk)
|
||||
bid_main.tasks.send_mail_password_changed(user_pk=user.pk)
|
||||
return user
|
||||
|
||||
|
||||
|
@ -39,7 +39,7 @@ def process_new_login(sender, request, user, **kwargs):
|
||||
fields.update({"last_login_ip", "current_login_ip"})
|
||||
|
||||
if user.has_confirmed_email:
|
||||
bid_main.tasks.send_new_user_session_email(
|
||||
bid_main.tasks.send_mail_new_user_session(
|
||||
user_pk=user.pk,
|
||||
session_data={
|
||||
'device': str(user_session.device or 'Unknown'),
|
||||
@ -83,7 +83,7 @@ def delete_orphaned_avatar_files(sender, instance, **kwargs):
|
||||
|
||||
|
||||
@receiver(recovery_used)
|
||||
def send_mfa_recovery_used_email(sender, **kwargs):
|
||||
def send_mail_mfa_recovery_used(sender, **kwargs):
|
||||
user = kwargs['device'].user
|
||||
if user.confirmed_email_at:
|
||||
bid_main.tasks.send_mfa_recovery_used_email(user.pk)
|
||||
bid_main.tasks.send_mail_mfa_recovery_used(user.pk)
|
||||
|
@ -12,7 +12,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
||||
def send_new_user_session_email(user_pk, session_data):
|
||||
def send_mail_new_user_session(user_pk, session_data):
|
||||
user = User.objects.get(pk=user_pk)
|
||||
log.info("sending a new user session email for account %s", user.pk)
|
||||
|
||||
@ -29,7 +29,7 @@ def send_new_user_session_email(user_pk, session_data):
|
||||
|
||||
|
||||
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
||||
def send_password_changed_email(user_pk):
|
||||
def send_mail_password_changed(user_pk):
|
||||
user = User.objects.get(pk=user_pk)
|
||||
log.info("sending a password change email for account %s", user.pk)
|
||||
|
||||
@ -46,7 +46,7 @@ def send_password_changed_email(user_pk):
|
||||
|
||||
|
||||
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
||||
def send_mfa_new_device_email(user_pk, device_type):
|
||||
def send_mail_mfa_new_device(user_pk, device_type):
|
||||
user = User.objects.get(pk=user_pk)
|
||||
log.info("sending a new mfa device email for account %s", user.pk)
|
||||
|
||||
@ -63,7 +63,7 @@ def send_mfa_new_device_email(user_pk, device_type):
|
||||
|
||||
|
||||
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
||||
def send_mfa_disabled_email(user_pk):
|
||||
def send_mail_mfa_disabled(user_pk):
|
||||
user = User.objects.get(pk=user_pk)
|
||||
log.info("sending an mfa disabled email for account %s", user.pk)
|
||||
|
||||
@ -80,7 +80,7 @@ def send_mfa_disabled_email(user_pk):
|
||||
|
||||
|
||||
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
||||
def send_mfa_recovery_used_email(user_pk):
|
||||
def send_mail_mfa_recovery_used(user_pk):
|
||||
user = User.objects.get(pk=user_pk)
|
||||
log.info("sending an mfa recovery used email for account %s", user.pk)
|
||||
|
||||
|
@ -3,7 +3,7 @@ Dear {{ user.full_name|default:user.email }}!
|
||||
|
||||
A new {{ device_type }} multi-factor authenticator has been added to your Blender ID account {{ user.email }}
|
||||
|
||||
If this wasn't done by you, please reset your password immediately and contact blenderid@blender.org for support.
|
||||
If this wasn't done by you, please reset your password immediately and contact {{ support_email }} for support.
|
||||
|
||||
--
|
||||
Kind regards,
|
||||
|
@ -3,7 +3,7 @@ Dear {{ user.full_name|default:user.email }}!
|
||||
|
||||
A recovery code was used to pass multi-factor authentication for your Blender ID account {{ user.email }}
|
||||
|
||||
If this wasn't done by you, please reset your password immediately, re-generate your MFA recovery codes, and contact blenderid@blender.org for support.
|
||||
If this wasn't done by you, please reset your password immediately, re-generate your MFA recovery codes, and contact {{ support_email }} for support.
|
||||
|
||||
--
|
||||
Kind regards,
|
||||
|
@ -60,7 +60,10 @@ Multi-factor Authentication Setup
|
||||
<div class="bid box mt-3">
|
||||
<h3>Security keys (U2F, WebAuthn, FIDO2)</h3>
|
||||
<p>
|
||||
E.g. a yubikey.
|
||||
Hardware security keys, e.g. Yubikeys.
|
||||
</p>
|
||||
<p>
|
||||
Blender ID supports these keys only as a second factor and <strong>does not</strong> provide a passwordless sign-in.
|
||||
</p>
|
||||
<ul>
|
||||
{% for d in devices_per_type.u2f %}
|
||||
@ -79,7 +82,7 @@ Multi-factor Authentication Setup
|
||||
<p>
|
||||
Oleg-Komarov marked this conversation as resolved
|
||||
Store your recovery codes safely (e.g. in a password manager or use a printed copy) and don't share them.
|
||||
Each code can be used only once.
|
||||
You can generate a new set of recovery codes at any time, any remaining old codes will become invalidated.
|
||||
You can generate a new set of recovery codes at any time, any remaining old codes will be invalidated.
|
||||
</p>
|
||||
{% with recovery=devices_per_type.recovery.0 %}
|
||||
{% if recovery %}
|
||||
|
@ -8,6 +8,14 @@ Multi-factor Authentication Setup
|
||||
{% block body %}
|
||||
<div class="bid box">
|
||||
<h2>New U2F device</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>Please watch <a href="https://www.youtube.com/watch?v=V6mxPS5O-sY">setup video</a> if you are not familiar with yubikeys.</p>
|
||||
{% if first_device %}
|
||||
<p>Since this is your first MFA device, you will be promted to use your security key immediately after setup to sign-in using MFA.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% with form=form|add_form_classes %}
|
||||
|
@ -12,8 +12,8 @@ import bid_main.tasks
|
||||
|
||||
|
||||
@patch(
|
||||
'bid_main.tasks.send_password_changed_email',
|
||||
new=bid_main.tasks.send_password_changed_email.task_function,
|
||||
'bid_main.tasks.send_mail_password_changed',
|
||||
new=bid_main.tasks.send_mail_password_changed.task_function,
|
||||
)
|
||||
@patch(
|
||||
'django.contrib.auth.base_user.AbstractBaseUser.check_password',
|
||||
|
@ -50,8 +50,8 @@ class TestActiveSessions(TestCase):
|
||||
|
||||
class TestNewUserSessionEmail(TestCase):
|
||||
@patch(
|
||||
'bid_main.tasks.send_new_user_session_email',
|
||||
new=bid_main.tasks.send_new_user_session_email.task_function,
|
||||
'bid_main.tasks.send_mail_new_user_session',
|
||||
new=bid_main.tasks.send_mail_new_user_session.task_function,
|
||||
)
|
||||
@patch(
|
||||
'django.contrib.auth.base_user.AbstractBaseUser.check_password',
|
||||
|
@ -69,7 +69,7 @@ class DisableView(mixins.MfaRequiredMixin, FormView):
|
||||
for device in devices_for_user(self.request.user):
|
||||
device.delete()
|
||||
if self.request.user.confirmed_email_at:
|
||||
bid_main.tasks.send_mfa_disabled_email(self.request.user.pk)
|
||||
bid_main.tasks.send_mail_mfa_disabled(self.request.user.pk)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ class GenerateRecoveryView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
):
|
||||
# Forbid setting up recovery codes unless the user already has some other method
|
||||
return HttpResponseBadRequest("can't setup recovery codes before other methods")
|
||||
user.staticdevice_set.all().delete()
|
||||
EncryptedRecoveryDevice.objects.filter(user=user).delete()
|
||||
device = EncryptedRecoveryDevice.objects.create(name='recovery', user=user)
|
||||
for _ in range(10):
|
||||
# https://pages.nist.gov/800-63-3/sp800-63b.html#5122-look-up-secret-verifiers
|
||||
@ -91,10 +91,10 @@ class GenerateRecoveryView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
return redirect(reverse('bid_main:mfa') + '?display_recovery_codes=1#recovery-codes')
|
||||
|
||||
|
||||
class InvalidateRecoveryView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
class InvalidateRecoveryView(mixins.MfaRequiredMixin, View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
user = self.request.user
|
||||
user.staticdevice_set.all().delete()
|
||||
EncryptedRecoveryDevice.objects.filter(user=user).delete()
|
||||
return redirect('bid_main:mfa')
|
||||
|
||||
|
||||
@ -132,7 +132,7 @@ class TotpRegisterView(mixins.MfaRequiredIfConfiguredMixin, FormView):
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
if self.request.user.confirmed_email_at:
|
||||
bid_main.tasks.send_mfa_new_device_email(self.request.user.pk, 'totp')
|
||||
bid_main.tasks.send_mail_mfa_new_device(self.request.user.pk, 'totp')
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
@ -141,6 +141,11 @@ class U2fRegisterView(mixins.MfaRequiredIfConfiguredMixin, FormView):
|
||||
success_url = reverse_lazy('bid_main:mfa')
|
||||
template_name = "bid_main/mfa/u2f_register.html"
|
||||
Oleg-Komarov marked this conversation as resolved
Anna Sirota
commented
is is `context['first_device'] = not devices_for_user(self.request.user)` necessary here as well?
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['first_device'] = not devices_for_user(self.request.user)
|
||||
return context
|
||||
|
||||
def get_form_kwargs(self):
|
||||
credentials = [
|
||||
AttestedCredentialData(d.credential)
|
||||
@ -161,7 +166,7 @@ class U2fRegisterView(mixins.MfaRequiredIfConfiguredMixin, FormView):
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
if self.request.user.confirmed_email_at:
|
||||
bid_main.tasks.send_mfa_new_device_email(self.request.user.pk, 'u2f')
|
||||
bid_main.tasks.send_mail_mfa_new_device(self.request.user.pk, 'u2f')
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
|
@ -428,3 +428,6 @@ if os.environ.get('ADMINS') is not None:
|
||||
ADMINS = [[_.strip() for _ in adm.split(':')] for adm in os.environ.get('ADMINS').split(',')]
|
||||
EMAIL_SUBJECT_PREFIX = f'[{ALLOWED_HOSTS[0]}]'
|
||||
SERVER_EMAIL = f'django@{ALLOWED_HOSTS[0]}'
|
||||
|
||||
|
||||
SUPPORT_EMAIL = 'blenderid@blender.org'
|
||||
|
@ -132,9 +132,6 @@ class U2fAuthenticateForm(OTPAgentFormMixin, forms.Form):
|
||||
self.clean_agent()
|
||||
return self.cleaned_data
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
|
||||
Oleg-Komarov marked this conversation as resolved
Anna Sirota
commented
might not be necessary at all, since this isn't a might not be necessary at all, since this isn't a `ModelForm`?
|
||||
class DisableMfaForm(forms.Form):
|
||||
disable_mfa_confirm = forms.BooleanField(
|
||||
|
Loading…
Reference in New Issue
Block a user
"will be invalided" or "will become invalid"