diff --git a/admin/common_auth/forms.py b/admin/common_auth/forms.py index aed87e67a6d..8dd9c6105ca 100644 --- a/admin/common_auth/forms.py +++ b/admin/common_auth/forms.py @@ -32,3 +32,14 @@ class DeskUserForm(forms.ModelForm): class Meta: model = AdminProfile fields = ['desk_token', 'desk_token_secret'] + + +class TwoFactorForm(forms.Form): + guid = forms.CharField(label='Guid', required=True, widget=forms.HiddenInput()) + code = forms.CharField( + label='Two-Factor Code', + required=True, + max_length=6, + min_length=6, + widget=forms.TextInput(attrs={'autocomplete': 'off'}) + ) diff --git a/admin/common_auth/views.py b/admin/common_auth/views.py index 0ccf53c7c25..552eb405f05 100644 --- a/admin/common_auth/views.py +++ b/admin/common_auth/views.py @@ -1,6 +1,6 @@ from django.urls import reverse, reverse_lazy from django.http import Http404 -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect @@ -11,7 +11,7 @@ from osf.models.user import OSFUser from osf.models import AdminProfile -from admin.common_auth.forms import LoginForm, UserRegistrationForm, DeskUserForm +from admin.common_auth.forms import LoginForm, UserRegistrationForm, DeskUserForm, TwoFactorForm class LoginView(FormView): @@ -24,20 +24,88 @@ class LoginView(FormView): def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) - def form_valid(self, form): - user = authenticate( - username=form.cleaned_data.get('email').strip(), - password=form.cleaned_data.get('password').strip() - ) - if user is not None: - login(self.request, user) + def get_form_class(self): + if self.request.method == 'POST': + if 'code' in self.request.POST: + return TwoFactorForm + + return LoginForm + + def post(self, request, *args, **kwargs): + form = self.get_context_data()['form'] + if isinstance(form, LoginForm): + error_message = 'Email and/or Password incorrect. Please try again.' + else: + error_message = 'Invalid two-factor code. Please try again.' + if 'guid' not in form.data: + error_message = 'Email and/or Password incorrect. Please try again.' + + if not form.is_valid(): + messages.error(self.request, error_message) + return redirect('auth:login') + + email = form.cleaned_data.get('email', '').strip() + password = form.cleaned_data.get('password', '').strip() + guid = form.cleaned_data.get('guid', '') + if isinstance(form, LoginForm): + user = authenticate(username=email, password=password) else: + user = OSFUser.load(guid) + + if not user: + messages.error(request, error_message) + return redirect('auth:login') + + # login and two-factor auth is not possible without having two-factor auth enabled + two_factor_settings = user.enabled_two_factor_settings + if not two_factor_settings: messages.error( - self.request, - 'Email and/or Password incorrect. Please try again.' + request, + 'Two-factor authentication must be enabled.' ) return redirect('auth:login') - return super().form_valid(form) + + # to not lose user after login request, we save its guid + # and use HiddenInput to not display it + if isinstance(form, LoginForm): + self.form_class = TwoFactorForm + return render( + request, + 'two_factor.html', + { + 'form': self.form_class( + initial={ + 'guid': str(user._id), + } + ) + } + ) + + # two-factor section + is_valid_code = two_factor_settings.verify_code(form.cleaned_data.get('code')) + if not is_valid_code: + messages.error( + self.request, + 'Invalid two-factor code. Please try again.' + ) + self.form_class = TwoFactorForm + return render( + request, + 'two_factor.html', + { + 'form': self.form_class( + initial={ + 'guid': str(user._id) + } + ) + } + ) + + # during 2FA step we don't authenticate user via authenticate(), + # so need to specify backend to set all appropriate attributes to the user correctly + user.backend = 'api.base.authentication.backends.ODMBackend' + login(self.request, user) + return super().post(request, *args, **kwargs) def get_success_url(self): redirect_to = self.request.GET.get(self.redirect_field_name, '') diff --git a/admin/templates/login.html b/admin/templates/login.html index 05dade5cb68..b66bef51619 100644 --- a/admin/templates/login.html +++ b/admin/templates/login.html @@ -45,6 +45,7 @@
+
{% if messages %}