Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions admin/common_auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'})
)
92 changes: 80 additions & 12 deletions admin/common_auth/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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, '')
Expand Down
1 change: 1 addition & 0 deletions admin/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
</div><!-- /.col -->
</div>
<div>
<br>
{% if messages %}
<ul class="register_page_messages">
{% for message in messages %}
Expand Down
59 changes: 59 additions & 0 deletions admin/templates/two_factor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{% extends 'base.html' %}
{% load render_bundle from webpack_loader %}

{% block title %}
<title>OSFAdmin | Log in</title>
{% endblock %}

<!-- hide navbar, sidebar, and User account menu -->
{% block navbar %}
{% endblock %}
{% block sidebar %}
{% endblock %}

{% block content %}

<div class="login-page">
<div class="login-box">
<div class="login-logo">
<b>OSF</b>Admin</a>
</div>
<div class="login-box-body">
<p class="login-box-msg">A verification code has been sent to your authenticator app.</p>
<form action="" method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<ul>
{% for error in form.non_field_errors %}
<li class="errorclass">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<div class="form-group">
<label for="code">Verification code:</label>
<p>{{ form.code }}</p>
{% if form.code.errors %}<span class="text-danger"> {{ form.code.errors }}</span>{% endif %}
</div>
{{ form.guid }}
<div class="row">
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">Sign In</button>
</div>
</div>
<div>
<br>
{% if messages %}
<ul class="two_factor_page_messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</form>
</div>
<br>
</div>
</div>
{% endblock %}
Loading
Loading