Commit 5452de20 by Carson Gee

Modified ssl certificate authentication to handle next redirection

Makes small changes in lms and cms both so that user's go to the
original page they intended to if they weren't already logged in
parent 6963d24e
......@@ -9,8 +9,9 @@ from django.conf import settings
from edxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut, ssl_get_cert_from_request
from microsite_configuration import microsite
from external_auth.views import (ssl_login_shortcut, ssl_get_cert_from_request,
redirect_with_get)
from microsite_configuration.middleware import MicrositeConfiguration
__all__ = ['signup', 'login_page', 'howitworks']
......@@ -26,7 +27,7 @@ def signup(request):
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to course to login to process their certificate if SSL is enabled
# and registration is disabled.
return redirect(reverse('login'))
return redirect_with_get('login', request.GET, False)
return render_to_response('register.html', {'csrf': csrf_token})
......@@ -43,11 +44,14 @@ def login_page(request):
# SSL login doesn't require a login view, so redirect
# to course now that the user is authenticated via
# the decorator.
return redirect('/course')
next_url = request.GET.get('next')
if next_url:
return redirect(next_url)
else:
return redirect('/course')
if settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there
return redirect(reverse('cas-login'))
return render_to_response(
'login.html',
{
......
......@@ -21,19 +21,29 @@ from mock import Mock
from edxmako.middleware import MakoMiddleware
from external_auth.models import ExternalAuthMap
import external_auth.views
from student.roles import CourseStaffRole
from student.tests.factories import UserFactory
from student.models import CourseEnrollment
from xmodule.modulestore import Location
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.exceptions import InsufficientSpecificationError
from xmodule.modulestore.tests.django_utils import (ModuleStoreTestCase,
mixed_store_config)
from xmodule.modulestore.tests.factories import CourseFactory
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP = FEATURES_WITH_SSL_AUTH.copy()
FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP['AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'] = True
FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE = FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP.copy()
FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
FEATURES_WITHOUT_SSL_AUTH = settings.FEATURES.copy()
FEATURES_WITHOUT_SSL_AUTH['AUTH_USE_CERTIFICATES'] = False
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH)
class SSLClientTest(TestCase):
class SSLClientTest(ModuleStoreTestCase):
"""
Tests SSL Authentication code sections of external_auth
"""
......@@ -170,7 +180,8 @@ class SSLClientTest(TestCase):
response = self.client.get(
reverse('dashboard'), follow=True,
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
self.assertIn(reverse('dashboard'), response['location'])
self.assertEquals(('http://testserver/dashboard', 302),
response.redirect_chain[-1])
self.assertIn(SESSION_KEY, self.client.session)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
......@@ -316,3 +327,70 @@ class SSLClientTest(TestCase):
self.assertEqual(1, len(ExternalAuthMap.objects.all()))
self.assertTrue(self.mock.called)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE,
MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
def test_ssl_lms_redirection(self):
"""
Auto signup auth user and ensure they return to the original
url they visited after being logged in.
"""
course = CourseFactory.create(
org='MITx',
number='999',
display_name='Robot Super Course'
)
external_auth.views.ssl_login(self._create_ssl_request('/'))
user = User.objects.get(email=self.USER_EMAIL)
CourseEnrollment.enroll(user, course.id)
course_private_url = '/courses/MITx/999/Robot_Super_Course/courseware'
self.assertFalse(SESSION_KEY in self.client.session)
response = self.client.get(
course_private_url,
follow=True,
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL),
HTTP_ACCEPT='text/html'
)
self.assertEqual(('http://testserver{0}'.format(course_private_url), 302),
response.redirect_chain[-1])
self.assertIn(SESSION_KEY, self.client.session)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE)
def test_ssl_cms_redirection(self):
"""
Auto signup auth user and ensure they return to the original
url they visited after being logged in.
"""
course = CourseFactory.create(
org='MITx',
number='999',
display_name='Robot Super Course'
)
external_auth.views.ssl_login(self._create_ssl_request('/'))
user = User.objects.get(email=self.USER_EMAIL)
CourseEnrollment.enroll(user, course.id)
CourseStaffRole(course.location).add_users(user)
location = Location(['i4x', 'MITx', '999', 'course',
Location.clean('Robot Super Course'), None])
new_location = loc_mapper().translate_location(
location.course_id, location, True, True
)
course_private_url = new_location.url_reverse('course/', '')
self.assertFalse(SESSION_KEY in self.client.session)
response = self.client.get(
course_private_url,
follow=True,
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL),
HTTP_ACCEPT='text/html'
)
self.assertEqual(('http://testserver{0}'.format(course_private_url), 302),
response.redirect_chain[-1])
self.assertIn(SESSION_KEY, self.client.session)
......@@ -440,7 +440,10 @@ def ssl_login(request):
(_user, email, fullname) = _ssl_dn_extract_info(cert)
retfun = functools.partial(redirect, '/')
redirect_to = request.GET.get('next')
if not redirect_to:
redirect_to = '/'
retfun = functools.partial(redirect, redirect_to)
return _external_login_or_signup(
request,
external_id=email,
......@@ -580,14 +583,14 @@ def course_specific_login(request, course_id):
course = course_from_id(course_id)
except ItemNotFoundError:
# couldn't find the course, will just return vanilla signin page
return _redirect_with_get_querydict('signin_user', request.GET)
return redirect_with_get('signin_user', request.GET)
# now the dispatching conditionals. Only shib for now
if settings.FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX):
return _redirect_with_get_querydict('shib-login', request.GET)
return redirect_with_get('shib-login', request.GET)
# Default fallthrough to normal signin page
return _redirect_with_get_querydict('signin_user', request.GET)
return redirect_with_get('signin_user', request.GET)
def course_specific_register(request, course_id):
......@@ -599,24 +602,28 @@ def course_specific_register(request, course_id):
course = course_from_id(course_id)
except ItemNotFoundError:
# couldn't find the course, will just return vanilla registration page
return _redirect_with_get_querydict('register_user', request.GET)
return redirect_with_get('register_user', request.GET)
# now the dispatching conditionals. Only shib for now
if settings.FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX):
# shib-login takes care of both registration and login flows
return _redirect_with_get_querydict('shib-login', request.GET)
return redirect_with_get('shib-login', request.GET)
# Default fallthrough to normal registration page
return _redirect_with_get_querydict('register_user', request.GET)
return redirect_with_get('register_user', request.GET)
def _redirect_with_get_querydict(view_name, get_querydict):
def redirect_with_get(view_name, get_querydict, do_reverse=True):
"""
Helper function to carry over get parameters across redirects
Using urlencode(safe='/') because the @login_required decorator generates 'next' queryparams with '/' unencoded
"""
if do_reverse:
url = reverse(view_name)
else:
url = view_name
if get_querydict:
return redirect("%s?%s" % (reverse(view_name), get_querydict.urlencode(safe='/')))
return redirect("%s?%s" % (url, get_querydict.urlencode(safe='/')))
return redirect(view_name)
......
......@@ -328,7 +328,7 @@ def signin_user(request):
# SSL login doesn't require a view, so redirect
# branding and allow that to process the login if it
# is enabled and the header is in the request.
return redirect(reverse('root'))
return external_auth.views.redirect_with_get('root', request.GET)
if settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there
return redirect(reverse('cas-login'))
......@@ -361,7 +361,7 @@ def register_user(request, extra_context=None):
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled
# and registration is disabled.
return redirect(reverse('root'))
return external_auth.views.redirect_with_get('root', request.GET)
context = {
'course_id': request.GET.get('course_id'),
......@@ -686,7 +686,7 @@ def accounts_login(request):
if settings.FEATURES['AUTH_USE_CERTIFICATES']:
# SSL login doesn't require a view, so redirect
# to branding and allow that to process the login.
return redirect(reverse('root'))
return external_auth.views.redirect_with_get('root', request.GET)
# see if the "next" parameter has been set, whether it has a course context, and if so, whether
# there is a course-specific place to redirect
redirect_to = request.GET.get('next')
......@@ -776,7 +776,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
# This is actually the common case, logging in user without external linked login
AUDIT_LOG.info("User %s w/o external auth attempting login", user)
# see if account has been locked out due to excessive login failures
# see if account has been locked out due to excessive login failres
user_found_by_email_lookup = user
if user_found_by_email_lookup and LoginFailures.is_feature_enabled():
if LoginFailures.is_user_locked_out(user_found_by_email_lookup):
......
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 to comment