Commit 21207ef7 by Kálmán Viktor

dashboard: 2fa auth after saml2/password login

parent 3e213c2d
...@@ -25,7 +25,9 @@ from django.shortcuts import redirect ...@@ -25,7 +25,9 @@ from django.shortcuts import redirect
from circle.settings.base import get_env_variable from circle.settings.base import get_env_variable
from dashboard.views import circle_login, HelpView, ResizeHelpView from dashboard.views import (
CircleLoginView, HelpView, ResizeHelpView, TwoFactorLoginView
)
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
from firewall.views import add_blacklist_item from firewall.views import add_blacklist_item
...@@ -52,8 +54,12 @@ urlpatterns = patterns( ...@@ -52,8 +54,12 @@ urlpatterns = patterns(
{'password_reset_form': CirclePasswordResetForm}, {'password_reset_form': CirclePasswordResetForm},
name="accounts.password-reset", name="accounts.password-reset",
), ),
url(r'^accounts/login/?$', circle_login, name="accounts.login"), url(r'^accounts/login/?$', CircleLoginView.as_view(),
name="accounts.login"),
url(r'^accounts/', include('django.contrib.auth.urls')), url(r'^accounts/', include('django.contrib.auth.urls')),
url(r'^two-factor-login/$', TwoFactorLoginView.as_view(),
name="two-factor-login"),
url(r'^info/help/$', HelpView.as_view(template_name="info/help.html"), url(r'^info/help/$', HelpView.as_view(template_name="info/help.html"),
name="info.help"), name="info.help"),
url(r'^info/policy/$', url(r'^info/policy/$',
......
...@@ -1686,3 +1686,19 @@ class DisableTwoFactorForm(ModelForm): ...@@ -1686,3 +1686,19 @@ class DisableTwoFactorForm(ModelForm):
totp = pyotp.TOTP(self.instance.two_factor_secret) totp = pyotp.TOTP(self.instance.two_factor_secret)
if not totp.verify(self.cleaned_data.get('confirmation_code')): if not totp.verify(self.cleaned_data.get('confirmation_code')):
raise ValidationError(_("Invalid confirmation code.")) raise ValidationError(_("Invalid confirmation code."))
class TwoFactorAuthForm(forms.Form):
confirmation_code = forms.CharField(
label=_('Confirmation code'),
help_text=_("Get the code from your authenticator to disable "
"two-factor authentication."))
def __init__(self, user, *args, **kwargs):
self.user = user
super(TwoFactorAuthForm, self).__init__(*args, **kwargs)
def clean_confirmation_code(self):
totp = pyotp.TOTP(self.user.profile.two_factor_secret)
if not totp.verify(self.cleaned_data.get('confirmation_code')):
raise ValidationError(_("Invalid confirmation code."))
...@@ -399,9 +399,6 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -399,9 +399,6 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
pre_user_save.connect(save_org_id) pre_user_save.connect(save_org_id)
else:
logger.debug("Do not register save_org_id to djangosaml2 pre_user_save")
def update_store_profile(sender, **kwargs): def update_store_profile(sender, **kwargs):
profile = kwargs.get('instance') profile = kwargs.get('instance')
......
...@@ -23,9 +23,8 @@ import pyotp ...@@ -23,9 +23,8 @@ import pyotp
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login from django.contrib.auth import login, authenticate
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.views import login as login_view
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core import signing from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
...@@ -38,7 +37,7 @@ from django.templatetags.static import static ...@@ -38,7 +37,7 @@ from django.templatetags.static import static
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
TemplateView, View, UpdateView, CreateView, TemplateView, View, UpdateView, CreateView, FormView
) )
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
...@@ -51,14 +50,15 @@ from vm.models import Instance, InstanceTemplate ...@@ -51,14 +50,15 @@ from vm.models import Instance, InstanceTemplate
from ..forms import ( from ..forms import (
CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm, CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm,
UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm, UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm,
UserListSearchForm, UserEditForm, TwoFactorForm, DisableTwoFactorForm UserListSearchForm, UserEditForm, TwoFactorForm, DisableTwoFactorForm,
TwoFactorAuthForm,
) )
from ..models import Profile, GroupProfile, ConnectCommand from ..models import Profile, GroupProfile, ConnectCommand
from ..tables import ( from ..tables import (
UserKeyListTable, ConnectCommandListTable, UserListTable, UserKeyListTable, ConnectCommandListTable, UserListTable,
) )
from .util import saml_available, DeleteViewBase from .util import saml_available, DeleteViewBase, LoginView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -98,17 +98,29 @@ class NotificationView(LoginRequiredMixin, TemplateView): ...@@ -98,17 +98,29 @@ class NotificationView(LoginRequiredMixin, TemplateView):
return response return response
def circle_login(request): class CircleLoginView(LoginView):
authentication_form = CircleAuthenticationForm form_class = CircleAuthenticationForm
extra_context = {
'saml2': saml_available, def get_context_data(self, **kwargs):
'og_image': (settings.DJANGO_URL.rstrip("/") + ctx = super(CircleLoginView, self).get_context_data(**kwargs)
static("dashboard/img/og.png")) ctx.update({
} 'saml2': saml_available,
response = login_view(request, authentication_form=authentication_form, 'og_image': (settings.DJANGO_URL.rstrip("/") +
extra_context=extra_context) static("dashboard/img/og.png"))
set_language_cookie(request, response) })
return response return ctx
def form_valid(self, form):
user = form.get_user()
if user.profile.two_factor_secret:
self.request.session['two-fa-user'] = user.pk
self.request.session['two-fa-redirect'] = self.get_success_url()
self.request.session['login-type'] = "password"
return redirect(reverse("two-factor-login"))
else:
response = super(CircleLoginView, self).form_valid(form)
set_language_cookie(self.request, response)
return response
class TokenLogin(View): class TokenLogin(View):
...@@ -589,3 +601,134 @@ class DisableTwoFactorView(LoginRequiredMixin, UpdateView): ...@@ -589,3 +601,134 @@ class DisableTwoFactorView(LoginRequiredMixin, UpdateView):
raise PermissionDenied raise PermissionDenied
return self.request.user.profile return self.request.user.profile
class TwoFactorLoginView(FormView):
form_class = TwoFactorAuthForm
template_name = "registration/two-factor-login.html"
def dispatch(self, *args, **kwargs):
if not self.request.session.get('two-fa-user'):
return redirect("/")
return super(TwoFactorLoginView, self).dispatch(*args, **kwargs)
def get_form_kwargs(self):
kwargs = super(TwoFactorLoginView, self).get_form_kwargs()
user_pk = self.request.session['two-fa-user']
kwargs['user'] = User.objects.get(pk=user_pk)
return kwargs
def form_valid(self, form):
user_pk = self.request.session['two-fa-user']
user = User.objects.get(pk=user_pk)
if self.request.session['login-type'] == "saml2":
user.backend = 'common.backends.Saml2Backend'
else:
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(self.request, user)
response = redirect(self.request.session['two-fa-redirect'])
set_language_cookie(self.request, response)
return response
if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
from djangosaml2.conf import get_config
from djangosaml2.signals import post_authenticated
from djangosaml2.utils import get_custom_setting
from saml2.client import Saml2Client
from saml2.ident import code
from saml2 import BINDING_HTTP_POST
@require_POST
@csrf_exempt
def circle_assertion_consumer_service(request,
config_loader_path=None,
attribute_mapping=None,
create_unknown_user=None):
"""SAML Authorization Response endpoint
The IdP will send its response to this view, which
will process it with pysaml2 help and log the user
in using the custom Authorization backend or redirect to 2fa
djangosaml2.backends.Saml2Backend that should be
enabled in the settings.py
"""
attribute_mapping = attribute_mapping or get_custom_setting(
'SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
create_unknown_user = create_unknown_user or get_custom_setting(
'SAML_CREATE_UNKNOWN_USER', True)
logger.debug('Assertion Consumer Service started')
conf = get_config(config_loader_path, request)
if 'SAMLResponse' not in request.POST:
return HttpResponseBadRequest(
'Couldn\'t find "SAMLResponse" in POST data.')
xmlstr = request.POST['SAMLResponse']
client = Saml2Client(
conf, identity_cache=IdentityCache(request.session))
oq_cache = OutstandingQueriesCache(request.session)
outstanding_queries = oq_cache.outstanding_queries()
# process the authentication response
response = client.parse_authn_request_response(
xmlstr, BINDING_HTTP_POST, outstanding_queries)
if response is None:
logger.error('SAML response is None')
return HttpResponseBadRequest(
"SAML response has errors. Please check the logs")
session_id = response.session_id()
oq_cache.delete(session_id)
# authenticate the remote user
session_info = response.session_info()
if callable(attribute_mapping):
attribute_mapping = attribute_mapping()
if callable(create_unknown_user):
create_unknown_user = create_unknown_user()
logger.debug('Trying to authenticate the user')
user = authenticate(session_info=session_info,
attribute_mapping=attribute_mapping,
create_unknown_user=create_unknown_user)
if user is None:
logger.error('The user is None')
return HttpResponseForbidden("Permission denied")
# redirect the user to the view where he came from
relay_state = request.POST.get('RelayState', '/')
if not relay_state:
logger.warning('The RelayState parameter exists but is empty')
relay_state = settings.LOGIN_REDIRECT_URL
logger.debug('Redirecting to the RelayState: ' + relay_state)
if user.profile.two_factor_secret:
request.session['two-fa-user'] = user.pk
request.session['two-fa-redirect'] = relay_state
request.session['login-type'] = "saml2"
return redirect(reverse("two-factor-login"))
else:
login(request, user)
def _set_subject_id(session, subject_id):
session['_saml2_subject_id'] = code(subject_id)
_set_subject_id(request.session, session_info['name_id'])
logger.debug('Sending the post_authenticated signal')
post_authenticated.send_robust(sender=user,
session_info=session_info)
# redirect the user to the view where he came from
return redirect(relay_state)
from djangosaml2 import views as saml2_views
saml2_views.assertion_consumer_service = circle_assertion_consumer_service
...@@ -23,19 +23,28 @@ from collections import OrderedDict ...@@ -23,19 +23,28 @@ from collections import OrderedDict
from urlparse import urljoin from urlparse import urljoin
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.views import redirect_to_login
from django.contrib.sites.shortcuts import get_current_site
from django.core import signing from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.db.models import Q from django.db.models import Q
from django.http import ( from django.http import (
HttpResponse, Http404, HttpResponseRedirect, JsonResponse HttpResponse, Http404, HttpResponseRedirect, JsonResponse
) )
from django.shortcuts import redirect, render from django.shortcuts import redirect, render, resolve_url
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View, DeleteView
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import DetailView, View, DeleteView, FormView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from braces.views import LoginRequiredMixin from braces.views import LoginRequiredMixin
...@@ -747,3 +756,61 @@ class DeleteViewBase(LoginRequiredMixin, DeleteView): ...@@ -747,3 +756,61 @@ class DeleteViewBase(LoginRequiredMixin, DeleteView):
else: else:
messages.success(request, self.success_message) messages.success(request, self.success_message)
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
# only in Django 1.9
class LoginView(FormView):
"""
Displays the login form and handles the login action.
"""
form_class = AuthenticationForm
authentication_form = None
redirect_field_name = REDIRECT_FIELD_NAME
template_name = 'registration/login.html'
redirect_authenticated_user = False
extra_context = None
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if (self.redirect_authenticated_user and
self.request.user.is_authenticated):
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_success_url(self):
"""Ensure the user-originating redirection URL is safe."""
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
if not is_safe_url(url=redirect_to, host=self.request.get_host()):
return resolve_url(settings.LOGIN_REDIRECT_URL)
return redirect_to
def get_form_class(self):
return self.authentication_form or self.form_class
def form_valid(self, form):
"""Security check complete. Log the user in."""
auth_login(self.request, form.get_user())
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
context = super(LoginView, self).get_context_data(**kwargs)
current_site = get_current_site(self.request)
context.update({
self.redirect_field_name: self.get_success_url(),
'site': current_site,
'site_name': current_site.name,
})
if self.extra_context is not None:
context.update(self.extra_context)
return context
{% extends "registration/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title-page %}{% trans "Login" %}{% endblock %}
{% block content_box %}
<div class="row">
<div class="col-md-12">
<form action="" method="POST">
{% csrf_token %}
{{ form.confirmation_code|as_crispy_field }}
<input type="submit"/>
</form>
</div>
</div>
<style>
.help-block {
display: block;
}
</style>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment