Initial mfa support (for internal users) #93591
@ -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>
|
||||||
|
@ -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()
|
credentials = [
|
||||||
|
AttestedCredentialData(d.credential)
|
||||||
|
for d in U2fDevice.objects.filter(user=self.request.user).all()
|
||||||
|
]
|
||||||
rp_id = self.request.get_host().split(':')[0] # remove port, required by webauthn
|
rp_id = self.request.get_host().split(':')[0] # remove port, required by webauthn
|
||||||
Oleg-Komarov marked this conversation as resolved
|
|||||||
|
credential_creation_options, state = register_begin(
|
||||||
|
rp_id, self.request.user, credentials,
|
||||||
|
)
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['credential_creation_options'] = json.dumps(dict(credential_creation_options))
|
||||||
kwargs['rp_id'] = rp_id
|
kwargs['rp_id'] = rp_id
|
||||||
|
kwargs['state'] = dict(state)
|
||||||
kwargs['user'] = self.request.user
|
kwargs['user'] = self.request.user
|
||||||
|
|
||||||
if self.request.method == 'GET':
|
|
||||||
credentials = [
|
|
||||||
AttestedCredentialData(d.credential)
|
|
||||||
for d in U2fDevice.objects.filter(user=self.request.user).all()
|
|
||||||
]
|
|
||||||
credential_creation_options, state = register_begin(
|
|
||||||
rp_id, self.request.user, credentials,
|
|
||||||
)
|
|
||||||
self.request.session['u2f_register_state'] = dict(state)
|
|
||||||
kwargs['credential_creation_options'] = json.dumps(dict(credential_creation_options))
|
|
||||||
if self.request.method == 'POST':
|
|
||||||
kwargs['state'] = self.request.session.pop('u2f_register_state', None)
|
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
17
mfa/forms.py
17
mfa/forms.py
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user
is
context['first_device'] = not devices_for_user(self.request.user)
necessary here as well?