Initial mfa support (for internal users) #93591

Merged
Oleg-Komarov merged 46 commits from mfa into main 2024-08-29 11:44:06 +02:00
5 changed files with 34 additions and 20 deletions
Showing only changes of commit f92acdfc37 - Show all commits

View File

@ -12,10 +12,16 @@ Multi-factor Authentication Setup
<div class="col-md-6"> <div class="col-md-6">
{% with form=form|add_form_classes %} {% with form=form|add_form_classes %}
<form method="post" id="u2f-register-form">{% csrf_token %} <form method="post" id="u2f-register-form">{% csrf_token %}
{% with field=form.credential %}
{% include "components/forms/field.html" %}
{% endwith %}
{% with field=form.name %} {% with field=form.name %}
{% include "components/forms/field.html" %} {% include "components/forms/field.html" %}
{% endwith %} {% endwith %}
{% with field=form.credential %} {% with field=form.signature %}
{% include "components/forms/field.html" %}
{% endwith %}
{% with field=form.state %}
{% include "components/forms/field.html" %} {% include "components/forms/field.html" %}
{% endwith %} {% endwith %}
<button type="submit" class="btn">Add security key</button> <button type="submit" class="btn">Add security key</button>

View File

@ -135,24 +135,19 @@ class U2fView(mixins.MfaRequiredIfConfiguredMixin, FormView):
template_name = "bid_main/mfa/u2f.html" template_name = "bid_main/mfa/u2f.html"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
rp_id = self.request.get_host().split(':')[0] # remove port, required by webauthn
kwargs['rp_id'] = rp_id
kwargs['user'] = self.request.user
if self.request.method == 'GET':
credentials = [ credentials = [
AttestedCredentialData(d.credential) AttestedCredentialData(d.credential)
for d in U2fDevice.objects.filter(user=self.request.user).all() for d in U2fDevice.objects.filter(user=self.request.user).all()
] ]
rp_id = self.request.get_host().split(':')[0] # remove port, required by webauthn
Oleg-Komarov marked this conversation as resolved
Review

is context['first_device'] = not devices_for_user(self.request.user) necessary here as well?

is `context['first_device'] = not devices_for_user(self.request.user)` necessary here as well?
credential_creation_options, state = register_begin( credential_creation_options, state = register_begin(
rp_id, self.request.user, credentials, rp_id, self.request.user, credentials,
) )
self.request.session['u2f_register_state'] = dict(state) kwargs = super().get_form_kwargs()
kwargs['credential_creation_options'] = json.dumps(dict(credential_creation_options)) kwargs['credential_creation_options'] = json.dumps(dict(credential_creation_options))
if self.request.method == 'POST': kwargs['rp_id'] = rp_id
kwargs['state'] = self.request.session.pop('u2f_register_state', None) kwargs['state'] = dict(state)
kwargs['user'] = self.request.user
return kwargs return kwargs
@transaction.atomic @transaction.atomic

View File

@ -111,8 +111,8 @@ class LoginView(mixins.RedirectToPrivacyAgreeMixin, mixins.PageIdMixin, otp_agen
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
devices = self.request.user.mfa_devices_per_category().get('u2f', None) devices = self.request.user.mfa_devices_per_category().get('u2f', None)
if devices and not self._use_recovery() and not self._use_totp(): if devices and not self._use_recovery() and not self._use_totp():
rp_id = self.request.get_host().split(':')[0] # remove port, required by webauthn
credentials = [AttestedCredentialData(d.credential) for d in devices] credentials = [AttestedCredentialData(d.credential) for d in devices]
rp_id = self.request.get_host().split(':')[0] # remove port, required by webauthn
request_options, state = authenticate_begin(rp_id, credentials) request_options, state = authenticate_begin(rp_id, credentials)
kwargs = self.get_form_kwargs() kwargs = self.get_form_kwargs()
kwargs['credentials'] = credentials kwargs['credentials'] = credentials

View File

@ -21,7 +21,7 @@ def get_fido2server(rp_id):
) )
def register_begin(rp_id, user, credentials=[]): def register_begin(rp_id, user, credentials):
return get_fido2server(rp_id).register_begin( return get_fido2server(rp_id).register_begin(
PublicKeyCredentialUserEntity( PublicKeyCredentialUserEntity(
display_name=user.email, display_name=user.email,

View File

@ -198,21 +198,34 @@ class U2fMfaForm(forms.Form):
attrs={"placeholder": "device name (for your convenience)"}, attrs={"placeholder": "device name (for your convenience)"},
), ),
) )
signature = forms.CharField(widget=forms.HiddenInput)
state = forms.JSONField(widget=forms.HiddenInput)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
credential_creation_options = kwargs.pop('credential_creation_options', None) credential_creation_options = kwargs.pop('credential_creation_options', None)
self.rp_id = kwargs.pop('rp_id', None) self.rp_id = kwargs.pop('rp_id', None)
self.state = kwargs.pop('state', None) state = kwargs.pop('state', None)
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
kwargs['initial']['signature'] = _sign(f"{self.user.email}_{state['challenge']}")
kwargs['initial']['state'] = state
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['credential'].widget.attrs['creation-options'] = credential_creation_options self.fields['credential'].widget.attrs['creation-options'] = credential_creation_options
def clean(self):
super().clean()
signature = self.cleaned_data.get('signature')
state = self.cleaned_data.get('state')
if not _verify_signature(f"{self.user.email}_{state['challenge']}", signature):
raise forms.ValidationError(_('Invalid signature'))
return self.cleaned_data
def save(self): def save(self):
credential = self.cleaned_data.get('credential') credential = self.cleaned_data.get('credential')
name = self.cleaned_data.get('name') name = self.cleaned_data.get('name')
state = self.cleaned_data.get('state')
auth_data = None auth_data = None
try: try:
auth_data = register_complete(self.rp_id, self.state, credential) auth_data = register_complete(self.rp_id, state, credential)
except Exception: except Exception:
raise forms.ValidationError(_('Verification failed')) raise forms.ValidationError(_('Verification failed'))
U2fDevice.objects.create( U2fDevice.objects.create(