From fd49f88e080692c0bb3704ee2151c31b436a69ca Mon Sep 17 00:00:00 2001
From: Nimisha Asthagiri <nasthagiri@edx.org>
Date: Wed, 5 Oct 2016 21:29:29 -0400
Subject: [PATCH] Moves external_auth from common to openedx/core.

---
 cms/djangoapps/contentstore/views/public.py                         |   7 +++++--
 cms/envs/common.py                                                  |   2 +-
 cms/urls.py                                                         |   2 +-
 common/djangoapps/external_auth/__init__.py                         |   0
 common/djangoapps/external_auth/admin.py                            |  13 -------------
 common/djangoapps/external_auth/djangostore.py                      | 123 ---------------------------------------------------------------------------------------------------------------------------
 common/djangoapps/external_auth/login_and_register.py               |  92 --------------------------------------------------------------------------------------------
 common/djangoapps/external_auth/migrations/0001_initial.py          |  34 ----------------------------------
 common/djangoapps/external_auth/migrations/__init__.py              |   0
 common/djangoapps/external_auth/models.py                           |  33 ---------------------------------
 common/djangoapps/external_auth/tests/__init__.py                   |   0
 common/djangoapps/external_auth/tests/test_helper.py                |  29 -----------------------------
 common/djangoapps/external_auth/tests/test_openid_provider.py       | 447 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 common/djangoapps/external_auth/tests/test_shib.py                  | 595 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 common/djangoapps/external_auth/tests/test_ssl.py                   | 417 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 common/djangoapps/external_auth/views.py                            | 947 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 common/djangoapps/student/tests/test_create_account.py              |   2 +-
 common/djangoapps/student/tests/test_login.py                       |   2 +-
 common/djangoapps/student/tests/test_password_policy.py             |   2 +-
 common/djangoapps/student/views.py                                  |  16 ++++++++--------
 docs/en_us/platform_api/source/conf.py                              |   2 +-
 lms/djangoapps/branding/views.py                                    |   2 +-
 lms/djangoapps/courseware/access.py                                 |   2 +-
 lms/djangoapps/dashboard/sysadmin.py                                |   4 ++--
 lms/djangoapps/lms_migration/management/commands/create_user.py     |   2 +-
 lms/djangoapps/student_account/views.py                             |   2 +-
 lms/envs/common.py                                                  |   2 +-
 lms/urls.py                                                         |  34 +++++++++++++++++++++++++---------
 openedx/core/djangoapps/external_auth/__init__.py                   |   0
 openedx/core/djangoapps/external_auth/admin.py                      |  16 ++++++++++++++++
 openedx/core/djangoapps/external_auth/djangostore.py                | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/login_and_register.py         |  98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/migrations/0001_initial.py    |  34 ++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/migrations/__init__.py        |   0
 openedx/core/djangoapps/external_auth/models.py                     |  35 +++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/tests/__init__.py             |   0
 openedx/core/djangoapps/external_auth/tests/test_helper.py          |  29 +++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/tests/test_openid_provider.py | 473 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/tests/test_shib.py            | 597 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/tests/test_ssl.py             | 418 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 openedx/core/djangoapps/external_auth/views.py                      | 954 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 41 files changed, 2837 insertions(+), 2762 deletions(-)
 delete mode 100644 common/djangoapps/external_auth/__init__.py
 delete mode 100644 common/djangoapps/external_auth/admin.py
 delete mode 100644 common/djangoapps/external_auth/djangostore.py
 delete mode 100644 common/djangoapps/external_auth/login_and_register.py
 delete mode 100644 common/djangoapps/external_auth/migrations/0001_initial.py
 delete mode 100644 common/djangoapps/external_auth/migrations/__init__.py
 delete mode 100644 common/djangoapps/external_auth/models.py
 delete mode 100644 common/djangoapps/external_auth/tests/__init__.py
 delete mode 100644 common/djangoapps/external_auth/tests/test_helper.py
 delete mode 100644 common/djangoapps/external_auth/tests/test_openid_provider.py
 delete mode 100644 common/djangoapps/external_auth/tests/test_shib.py
 delete mode 100644 common/djangoapps/external_auth/tests/test_ssl.py
 delete mode 100644 common/djangoapps/external_auth/views.py
 create mode 100644 openedx/core/djangoapps/external_auth/__init__.py
 create mode 100644 openedx/core/djangoapps/external_auth/admin.py
 create mode 100644 openedx/core/djangoapps/external_auth/djangostore.py
 create mode 100644 openedx/core/djangoapps/external_auth/login_and_register.py
 create mode 100644 openedx/core/djangoapps/external_auth/migrations/0001_initial.py
 create mode 100644 openedx/core/djangoapps/external_auth/migrations/__init__.py
 create mode 100644 openedx/core/djangoapps/external_auth/models.py
 create mode 100644 openedx/core/djangoapps/external_auth/tests/__init__.py
 create mode 100644 openedx/core/djangoapps/external_auth/tests/test_helper.py
 create mode 100644 openedx/core/djangoapps/external_auth/tests/test_openid_provider.py
 create mode 100644 openedx/core/djangoapps/external_auth/tests/test_shib.py
 create mode 100644 openedx/core/djangoapps/external_auth/tests/test_ssl.py
 create mode 100644 openedx/core/djangoapps/external_auth/views.py

diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index 1a2ca08..5aeb8c8 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -10,8 +10,11 @@ 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,
-                                 redirect_with_get)
+from openedx.core.djangoapps.external_auth.views import (
+    ssl_login_shortcut,
+    ssl_get_cert_from_request,
+    redirect_with_get,
+)
 from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
 
 __all__ = ['signup', 'login_page', 'howitworks']
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 1c82fa4..7eb296d 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -817,7 +817,7 @@ INSTALLED_APPS = (
     'contentstore',
     'contentserver',
     'course_creators',
-    'external_auth',
+    'openedx.core.djangoapps.external_auth',
     'student',  # misleading name due to sharing with lms
     'openedx.core.djangoapps.course_groups',  # not used in cms (yet), but tests run
     'openedx.core.djangoapps.coursetalk',  # not used in cms (yet), but tests run
diff --git a/cms/urls.py b/cms/urls.py
index d4e2c08..7b8f4fe 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -153,7 +153,7 @@ if settings.FEATURES.get('ENABLE_SERVICE_STATUS'):
 
 if settings.FEATURES.get('AUTH_USE_CAS'):
     urlpatterns += (
-        url(r'^cas-auth/login/$', 'external_auth.views.cas_login', name="cas-login"),
+        url(r'^cas-auth/login/$', 'openedx.core.djangoapps.external_auth.views.cas_login', name="cas-login"),
         url(r'^cas-auth/logout/$', 'django_cas.views.logout', {'next_page': '/'}, name="cas-logout"),
     )
 
diff --git a/common/djangoapps/external_auth/__init__.py b/common/djangoapps/external_auth/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/common/djangoapps/external_auth/__init__.py
+++ /dev/null
diff --git a/common/djangoapps/external_auth/admin.py b/common/djangoapps/external_auth/admin.py
deleted file mode 100644
index fad0917..0000000
--- a/common/djangoapps/external_auth/admin.py
+++ /dev/null
@@ -1,13 +0,0 @@
-'''
-django admin pages for courseware model
-'''
-
-from external_auth.models import *
-from ratelimitbackend import admin
-
-
-class ExternalAuthMapAdmin(admin.ModelAdmin):
-    search_fields = ['external_id', 'user__username']
-    date_hierarchy = 'dtcreated'
-
-admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin)
diff --git a/common/djangoapps/external_auth/djangostore.py b/common/djangoapps/external_auth/djangostore.py
deleted file mode 100644
index 8b830e0..0000000
--- a/common/djangoapps/external_auth/djangostore.py
+++ /dev/null
@@ -1,123 +0,0 @@
-"""A openid store using django cache"""
-
-from openid.store.interface import OpenIDStore
-from openid.store import nonce
-
-from django.core.cache import cache
-
-import logging
-import time
-
-DEFAULT_ASSOCIATIONS_TIMEOUT = 60
-DEFAULT_NONCE_TIMEOUT = 600
-
-ASSOCIATIONS_KEY_PREFIX = 'openid.provider.associations.'
-NONCE_KEY_PREFIX = 'openid.provider.nonce.'
-
-log = logging.getLogger('DjangoOpenIDStore')
-
-
-def get_url_key(server_url):
-    key = ASSOCIATIONS_KEY_PREFIX + server_url
-    return key
-
-
-def get_nonce_key(server_url, timestamp, salt):
-    key = '{prefix}{url}.{ts}.{salt}'.format(prefix=NONCE_KEY_PREFIX,
-                                             url=server_url,
-                                             ts=timestamp,
-                                             salt=salt)
-    return key
-
-
-class DjangoOpenIDStore(OpenIDStore):
-    def __init__(self):
-        log.info('DjangoStore cache:' + str(cache.__class__))
-
-    def storeAssociation(self, server_url, assoc):
-        key = get_url_key(server_url)
-
-        log.info('storeAssociation {0}'.format(key))
-
-        associations = cache.get(key, {})
-        associations[assoc.handle] = assoc
-
-        cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
-
-    def getAssociation(self, server_url, handle=None):
-        key = get_url_key(server_url)
-
-        log.info('getAssociation {0}'.format(key))
-
-        associations = cache.get(key, {})
-
-        assoc = None
-
-        if handle is None:
-            # get best association
-            valid_assocs = [a for a in associations if a.getExpiresIn() > 0]
-            if valid_assocs:
-                valid_assocs.sort(lambda a: a.getExpiresIn(), reverse=True)
-                assoc = valid_assocs.sort[0]
-        else:
-            assoc = associations.get(handle)
-
-        # check expiration and remove if it has expired
-        if assoc and assoc.getExpiresIn() <= 0:
-            if handle is None:
-                cache.delete(key)
-            else:
-                associations.pop(handle)
-                cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
-            assoc = None
-
-        return assoc
-
-    def removeAssociation(self, server_url, handle):
-        key = get_url_key(server_url)
-
-        log.info('removeAssociation {0}'.format(key))
-
-        associations = cache.get(key, {})
-
-        removed = False
-
-        if associations:
-            if handle is None:
-                cache.delete(key)
-                removed = True
-            else:
-                assoc = associations.pop(handle, None)
-                if assoc:
-                    cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
-                    removed = True
-
-        return removed
-
-    def useNonce(self, server_url, timestamp, salt):
-        key = get_nonce_key(server_url, timestamp, salt)
-
-        log.info('useNonce {0}'.format(key))
-
-        if abs(timestamp - time.time()) > nonce.SKEW:
-            return False
-
-        anonce = cache.get(key)
-
-        found = False
-
-        if anonce is None:
-            cache.set(key, '-', DEFAULT_NONCE_TIMEOUT)
-            found = False
-        else:
-            found = True
-
-        return found
-
-    def cleanupNonces(self):
-        # not necesary, keys will timeout
-        return 0
-
-    def cleanupAssociations(self):
-        # not necesary, keys will timeout
-        return 0
diff --git a/common/djangoapps/external_auth/login_and_register.py b/common/djangoapps/external_auth/login_and_register.py
deleted file mode 100644
index fa4ab2e..0000000
--- a/common/djangoapps/external_auth/login_and_register.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""Intercept login and registration requests.
-
-This module contains legacy code originally from `student.views`.
-"""
-import re
-
-from django.conf import settings
-from django.shortcuts import redirect
-from django.core.urlresolvers import reverse
-import external_auth.views
-
-from xmodule.modulestore.django import modulestore
-from opaque_keys.edx.keys import CourseKey
-
-
-# pylint: disable=fixme
-# TODO: This function is kind of gnarly/hackish/etc and is only used in one location.
-# It'd be awesome if we could get rid of it; manually parsing course_id strings form larger strings
-# seems Probably Incorrect
-def _parse_course_id_from_string(input_str):
-    """
-    Helper function to determine if input_str (typically the queryparam 'next') contains a course_id.
-    @param input_str:
-    @return: the course_id if found, None if not
-    """
-    m_obj = re.match(r'^/courses/{}'.format(settings.COURSE_ID_PATTERN), input_str)
-    if m_obj:
-        return CourseKey.from_string(m_obj.group('course_id'))
-    return None
-
-
-def _get_course_enrollment_domain(course_id):
-    """
-    Helper function to get the enrollment domain set for a course with id course_id
-    @param course_id:
-    @return:
-    """
-    course = modulestore().get_course(course_id)
-    if course is None:
-        return None
-
-    return course.enrollment_domain
-
-
-def login(request):
-    """Allow external auth to intercept and handle a login request.
-
-    Arguments:
-        request (Request): A request for the login page.
-
-    Returns:
-        Response or None
-
-    """
-    # Default to a `None` response, indicating that external auth
-    # is not handling the request.
-    response = None
-
-    if settings.FEATURES['AUTH_USE_CERTIFICATES'] and external_auth.views.ssl_get_cert_from_request(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.
-        response = external_auth.views.redirect_with_get('root', request.GET)
-    elif settings.FEATURES.get('AUTH_USE_CAS'):
-        # If CAS is enabled, redirect auth handling to there
-        response = redirect(reverse('cas-login'))
-    elif settings.FEATURES.get('AUTH_USE_SHIB'):
-        redirect_to = request.GET.get('next')
-        if redirect_to:
-            course_id = _parse_course_id_from_string(redirect_to)
-            if course_id and _get_course_enrollment_domain(course_id):
-                response = external_auth.views.course_specific_login(request, course_id.to_deprecated_string())
-
-    return response
-
-
-def register(request):
-    """Allow external auth to intercept and handle a registration request.
-
-    Arguments:
-        request (Request): A request for the registration page.
-
-    Returns:
-        Response or None
-
-    """
-    response = 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.
-        response = external_auth.views.redirect_with_get('root', request.GET)
-    return response
diff --git a/common/djangoapps/external_auth/migrations/0001_initial.py b/common/djangoapps/external_auth/migrations/0001_initial.py
deleted file mode 100644
index f68b8f0..0000000
--- a/common/djangoapps/external_auth/migrations/0001_initial.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-from django.conf import settings
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ExternalAuthMap',
-            fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('external_id', models.CharField(max_length=255, db_index=True)),
-                ('external_domain', models.CharField(max_length=255, db_index=True)),
-                ('external_credentials', models.TextField(blank=True)),
-                ('external_email', models.CharField(max_length=255, db_index=True)),
-                ('external_name', models.CharField(db_index=True, max_length=255, blank=True)),
-                ('internal_password', models.CharField(max_length=31, blank=True)),
-                ('dtcreated', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
-                ('dtsignup', models.DateTimeField(null=True, verbose_name=b'signup date')),
-                ('user', models.OneToOneField(null=True, to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.AlterUniqueTogether(
-            name='externalauthmap',
-            unique_together=set([('external_id', 'external_domain')]),
-        ),
-    ]
diff --git a/common/djangoapps/external_auth/migrations/__init__.py b/common/djangoapps/external_auth/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/common/djangoapps/external_auth/migrations/__init__.py
+++ /dev/null
diff --git a/common/djangoapps/external_auth/models.py b/common/djangoapps/external_auth/models.py
deleted file mode 100644
index 5630725..0000000
--- a/common/djangoapps/external_auth/models.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""
-WE'RE USING MIGRATIONS!
-
-If you make changes to this model, be sure to create an appropriate migration
-file and check it in at the same time as your model changes. To do that,
-
-1. Go to the edx-platform dir
-2. ./manage.py lms schemamigration student --auto description_of_your_change
-3. Add the migration file created in edx-platform/common/djangoapps/external_auth/migrations/
-"""
-
-from django.db import models
-from django.contrib.auth.models import User
-
-
-class ExternalAuthMap(models.Model):
-    class Meta(object):
-        app_label = "external_auth"
-        unique_together = (('external_id', 'external_domain'), )
-
-    external_id = models.CharField(max_length=255, db_index=True)
-    external_domain = models.CharField(max_length=255, db_index=True)
-    external_credentials = models.TextField(blank=True)  # JSON dictionary
-    external_email = models.CharField(max_length=255, db_index=True)
-    external_name = models.CharField(blank=True, max_length=255, db_index=True)
-    user = models.OneToOneField(User, unique=True, db_index=True, null=True)
-    internal_password = models.CharField(blank=True, max_length=31)  	# randomly generated
-    dtcreated = models.DateTimeField('creation date', auto_now_add=True)
-    dtsignup = models.DateTimeField('signup date', null=True)		# set after signup
-
-    def __unicode__(self):
-        s = "[%s] = (%s / %s)" % (self.external_id, self.external_name, self.external_email)
-        return s
diff --git a/common/djangoapps/external_auth/tests/__init__.py b/common/djangoapps/external_auth/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/common/djangoapps/external_auth/tests/__init__.py
+++ /dev/null
diff --git a/common/djangoapps/external_auth/tests/test_helper.py b/common/djangoapps/external_auth/tests/test_helper.py
deleted file mode 100644
index b7cacae..0000000
--- a/common/djangoapps/external_auth/tests/test_helper.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""
-Tests for utility functions in external_auth module
-"""
-from django.test import TestCase
-from external_auth.views import _safe_postlogin_redirect
-
-
-class ExternalAuthHelperFnTest(TestCase):
-    """
-    Unit tests for the external_auth.views helper function
-    """
-    def test__safe_postlogin_redirect(self):
-        """
-        Tests the _safe_postlogin_redirect function with different values of next
-        """
-        HOST = 'testserver'                               # pylint: disable=invalid-name
-        ONSITE1 = '/dashboard'                            # pylint: disable=invalid-name
-        ONSITE2 = '/courses/org/num/name/courseware'      # pylint: disable=invalid-name
-        ONSITE3 = 'http://{}/my/custom/url'.format(HOST)  # pylint: disable=invalid-name
-        OFFSITE1 = 'http://www.attacker.com'              # pylint: disable=invalid-name
-
-        for redirect_to in [ONSITE1, ONSITE2, ONSITE3]:
-            redir = _safe_postlogin_redirect(redirect_to, HOST)
-            self.assertEqual(redir.status_code, 302)
-            self.assertEqual(redir['location'], redirect_to)
-
-        redir2 = _safe_postlogin_redirect(OFFSITE1, HOST)
-        self.assertEqual(redir2.status_code, 302)
-        self.assertEqual("/", redir2['location'])
diff --git a/common/djangoapps/external_auth/tests/test_openid_provider.py b/common/djangoapps/external_auth/tests/test_openid_provider.py
deleted file mode 100644
index e2fff63..0000000
--- a/common/djangoapps/external_auth/tests/test_openid_provider.py
+++ /dev/null
@@ -1,447 +0,0 @@
-#-*- encoding=utf-8 -*-
-'''
-Created on Jan 18, 2013
-
-@author: brian
-'''
-import openid
-from openid.fetchers import HTTPFetcher, HTTPResponse
-from urlparse import parse_qs, urlparse
-
-from django.conf import settings
-from django.test import TestCase, LiveServerTestCase
-from django.core.cache import cache
-from django.test.utils import override_settings
-from django.core.urlresolvers import reverse
-from django.test.client import RequestFactory
-from unittest import skipUnless
-
-from student.tests.factories import UserFactory
-from external_auth.views import provider_login
-
-
-class MyFetcher(HTTPFetcher):
-    """A fetcher that uses server-internal calls for performing HTTP
-    requests.
-    """
-
-    def __init__(self, client):
-        """@param client: A test client object"""
-
-        super(MyFetcher, self).__init__()
-        self.client = client
-
-    def fetch(self, url, body=None, headers=None):
-        """Perform an HTTP request
-
-        @raises Exception: Any exception that can be raised by Django
-
-        @see: C{L{HTTPFetcher.fetch}}
-        """
-        if body:
-            # method = 'POST'
-            # undo the URL encoding of the POST arguments
-            data = parse_qs(body)
-            response = self.client.post(url, data)
-        else:
-            # method = 'GET'
-            data = {}
-            if headers and 'Accept' in headers:
-                data['CONTENT_TYPE'] = headers['Accept']
-            response = self.client.get(url, data)
-
-        # Translate the test client response to the fetcher's HTTP response abstraction
-        content = response.content
-        final_url = url
-        response_headers = {}
-        if 'Content-Type' in response:
-            response_headers['content-type'] = response['Content-Type']
-        if 'X-XRDS-Location' in response:
-            response_headers['x-xrds-location'] = response['X-XRDS-Location']
-        status = response.status_code
-
-        return HTTPResponse(
-            body=content,
-            final_url=final_url,
-            headers=response_headers,
-            status=status,
-        )
-
-
-class OpenIdProviderTest(TestCase):
-    """
-    Tests of the OpenId login
-    """
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_begin_login_with_xrds_url(self):
-
-        # the provider URL must be converted to an absolute URL in order to be
-        # used as an openid provider.
-        provider_url = reverse('openid-provider-xrds')
-        factory = RequestFactory()
-        request = factory.request()
-        abs_provider_url = request.build_absolute_uri(location=provider_url)
-
-        # In order for this absolute URL to work (i.e. to get xrds, then authentication)
-        # in the test environment, we either need a live server that works with the default
-        # fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
-        # Here we do the latter:
-        fetcher = MyFetcher(self.client)
-        openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
-
-        # now we can begin the login process by invoking a local openid client,
-        # with a pointer to the (also-local) openid provider:
-        with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
-
-            url = reverse('openid-login')
-            resp = self.client.post(url)
-            code = 200
-            self.assertEqual(resp.status_code, code,
-                             "got code {0} for url '{1}'. Expected code {2}"
-                             .format(resp.status_code, url, code))
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_begin_login_with_login_url(self):
-
-        # the provider URL must be converted to an absolute URL in order to be
-        # used as an openid provider.
-        provider_url = reverse('openid-provider-login')
-        factory = RequestFactory()
-        request = factory.request()
-        abs_provider_url = request.build_absolute_uri(location=provider_url)
-
-        # In order for this absolute URL to work (i.e. to get xrds, then authentication)
-        # in the test environment, we either need a live server that works with the default
-        # fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
-        # Here we do the latter:
-        fetcher = MyFetcher(self.client)
-        openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
-
-        # now we can begin the login process by invoking a local openid client,
-        # with a pointer to the (also-local) openid provider:
-        with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
-            url = reverse('openid-login')
-            resp = self.client.post(url)
-            code = 200
-            self.assertEqual(resp.status_code, code,
-                             "got code {0} for url '{1}'. Expected code {2}"
-                             .format(resp.status_code, url, code))
-            self.assertContains(resp, '<input name="openid.mode" type="hidden" value="checkid_setup" />', html=True)
-            self.assertContains(resp, '<input name="openid.ns" type="hidden" value="http://specs.openid.net/auth/2.0" />', html=True)
-            self.assertContains(resp, '<input name="openid.identity" type="hidden" value="http://specs.openid.net/auth/2.0/identifier_select" />', html=True)
-            self.assertContains(resp, '<input name="openid.claimed_id" type="hidden" value="http://specs.openid.net/auth/2.0/identifier_select" />', html=True)
-            self.assertContains(resp, '<input name="openid.ns.ax" type="hidden" value="http://openid.net/srv/ax/1.0" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.mode" type="hidden" value="fetch_request" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.required" type="hidden" value="email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.fullname" type="hidden" value="http://axschema.org/namePerson" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.lastname" type="hidden" value="http://axschema.org/namePerson/last" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.firstname" type="hidden" value="http://axschema.org/namePerson/first" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.nickname" type="hidden" value="http://axschema.org/namePerson/friendly" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.email" type="hidden" value="http://axschema.org/contact/email" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.old_email" type="hidden" value="http://schema.openid.net/contact/email" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.old_nickname" type="hidden" value="http://schema.openid.net/namePerson/friendly" />', html=True)
-            self.assertContains(resp, '<input name="openid.ax.type.old_fullname" type="hidden" value="http://schema.openid.net/namePerson" />', html=True)
-            self.assertContains(resp, '<input type="submit" value="Continue" />', html=True)
-            # this should work on the server:
-            self.assertContains(resp, '<input name="openid.realm" type="hidden" value="http://testserver/" />', html=True)
-
-            # not included here are elements that will vary from run to run:
-            # <input name="openid.return_to" type="hidden" value="http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H" />
-            # <input name="openid.assoc_handle" type="hidden" value="{HMAC-SHA1}{50ff8120}{rh87+Q==}" />
-
-    def attempt_login(self, expected_code, login_method='POST', **kwargs):
-        """ Attempt to log in through the open id provider login """
-        url = reverse('openid-provider-login')
-        args = {
-            "openid.mode": "checkid_setup",
-            "openid.return_to": "http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H",
-            "openid.assoc_handle": "{HMAC-SHA1}{50ff8120}{rh87+Q==}",
-            "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
-            "openid.ns": "http://specs.openid.net/auth/2.0",
-            "openid.realm": "http://testserver/",
-            "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
-            "openid.ns.ax": "http://openid.net/srv/ax/1.0",
-            "openid.ax.mode": "fetch_request",
-            "openid.ax.required": "email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname",
-            "openid.ax.type.fullname": "http://axschema.org/namePerson",
-            "openid.ax.type.lastname": "http://axschema.org/namePerson/last",
-            "openid.ax.type.firstname": "http://axschema.org/namePerson/first",
-            "openid.ax.type.nickname": "http://axschema.org/namePerson/friendly",
-            "openid.ax.type.email": "http://axschema.org/contact/email",
-            "openid.ax.type.old_email": "http://schema.openid.net/contact/email",
-            "openid.ax.type.old_nickname": "http://schema.openid.net/namePerson/friendly",
-            "openid.ax.type.old_fullname": "http://schema.openid.net/namePerson",
-        }
-        # override the default args with any given arguments
-        for key in kwargs:
-            args["openid." + key] = kwargs[key]
-
-        if login_method == 'POST':
-            resp = self.client.post(url, args)
-        elif login_method == 'GET':
-            resp = self.client.get(url, args)
-        else:
-            self.fail('Invalid login method')
-
-        code = expected_code
-        self.assertEqual(resp.status_code, code,
-                         "got code {0} for url '{1}'. Expected code {2}"
-                         .format(resp.status_code, url, code))
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_open_id_setup(self):
-        """ Attempt a standard successful login """
-        self.attempt_login(200)
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_invalid_namespace(self):
-        """ Test for 403 error code when the namespace of the request is invalid"""
-        self.attempt_login(403, ns="http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0")
-
-    @override_settings(OPENID_PROVIDER_TRUSTED_ROOTS=['http://apps.cs50.edx.org'])
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_invalid_return_url(self):
-        """ Test for 403 error code when the url"""
-        self.attempt_login(403, return_to="http://apps.cs50.edx.or")
-
-    def _send_bad_redirection_login(self):
-        """
-        Attempt to log in to the provider with setup parameters
-
-        Intentionally fail the login to force a redirect
-        """
-        user = UserFactory()
-
-        factory = RequestFactory()
-        post_params = {'email': user.email, 'password': 'password'}
-        fake_url = 'fake url'
-        request = factory.post(reverse('openid-provider-login'), post_params)
-        openid_setup = {
-            'request': factory.request(),
-            'url': fake_url,
-            'post_params': {}
-        }
-        request.session = {
-            'openid_setup': openid_setup
-        }
-        response = provider_login(request)
-        return response
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_login_openid_handle_redirection(self):
-        """ Test to see that we can handle login redirection properly"""
-        response = self._send_bad_redirection_login()
-        self.assertEquals(response.status_code, 302)
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_login_openid_handle_redirection_ratelimited(self):
-        # try logging in 30 times, the default limit in the number of failed
-        # log in attempts before the rate gets limited
-        for _ in xrange(30):
-            self._send_bad_redirection_login()
-
-        response = self._send_bad_redirection_login()
-        # verify that we are not returning the default 403
-        self.assertEquals(response.status_code, 302)
-        # clear the ratelimit cache so that we don't fail other logins
-        cache.clear()
-
-    def _attempt_login_and_perform_final_response(self, user, profile_name):
-        """
-        Performs full procedure of a successful OpenID provider login for user,
-        all required data is taken form ``user`` attribute which is an instance
-        of ``User`` model. As a convenience this method will also set
-        ``profile.name`` for the user.
-        """
-        url = reverse('openid-provider-login')
-
-        # login to the client so that we can persist session information
-        user.profile.name = profile_name
-        user.profile.save()
-        # It is asssumed that user's password is test (default for UserFactory)
-        self.client.login(username=user.username, password='test')
-        # login once to get the right session information
-        self.attempt_login(200)
-        post_args = {
-            'email': user.email,
-            'password': 'test'
-        }
-
-        # call url again, this time with username and password
-        return self.client.post(url, post_args)
-
-    @skipUnless(
-        settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'), 'OpenID not enabled')
-    def test_provider_login_can_handle_unicode_email(self):
-        user = UserFactory(email=u"user.ąęł@gmail.com")
-        resp = self._attempt_login_and_perform_final_response(user, u"Jan ĄĘŁ")
-        location = resp['Location']
-        parsed_url = urlparse(location)
-        parsed_qs = parse_qs(parsed_url.query)
-        self.assertEquals(parsed_qs['openid.ax.type.ext1'][0], 'http://axschema.org/contact/email')
-        self.assertEquals(parsed_qs['openid.ax.type.ext0'][0], 'http://axschema.org/namePerson')
-        self.assertEquals(parsed_qs['openid.ax.value.ext0.1'][0],
-                          user.profile.name.encode('utf-8'))  # pylint: disable=no-member
-        self.assertEquals(parsed_qs['openid.ax.value.ext1.1'][0],
-                          user.email.encode('utf-8'))  # pylint: disable=no-member
-
-    @skipUnless(
-        settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'), 'OpenID not enabled')
-    def test_provider_login_can_handle_unicode_email_invalid_password(self):
-        user = UserFactory(email=u"user.ąęł@gmail.com")
-        url = reverse('openid-provider-login')
-
-        # login to the client so that we can persist session information
-        user.profile.name = u"Jan ĄĘ"
-        user.profile.save()
-        # It is asssumed that user's password is test (default for UserFactory)
-        self.client.login(username=user.username, password='test')
-        # login once to get the right session information
-        self.attempt_login(200)
-        # We trigger situation where user password is invalid at last phase
-        # of openid login
-        post_args = {
-            'email': user.email,
-            'password': 'invalid-password'
-        }
-
-        # call url again, this time with username and password
-        return self.client.post(url, post_args)
-
-    @skipUnless(
-        settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'), 'OpenID not enabled')
-    def test_provider_login_can_handle_unicode_email_inactive_account(self):
-        user = UserFactory(email=u"user.ąęł@gmail.com", username=u"ąęół")
-        url = reverse('openid-provider-login')
-
-        # login to the client so that we can persist session information
-        user.profile.name = u'Jan ĄĘ'
-        user.profile.save()  # pylint: disable=no-member
-        self.client.login(username=user.username, password='test')
-        # login once to get the right session information
-        self.attempt_login(200)
-        # We trigger situation where user is not active at final phase of
-        # OpenId login.
-        user.is_active = False
-        user.save()  # pylint: disable=no-member
-        post_args = {
-            'email': user.email,
-            'password': 'test'
-        }
-        # call url again, this time with username and password
-        self.client.post(url, post_args)
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_openid_final_response(self):
-
-        user = UserFactory()
-
-        # login to the client so that we can persist session information
-        for name in ['Robot 33', '☃']:
-            resp = self._attempt_login_and_perform_final_response(user, name)
-            # all information is embedded in the redirect url
-            location = resp['Location']
-            # parse the url
-            parsed_url = urlparse(location)
-            parsed_qs = parse_qs(parsed_url.query)
-            self.assertEquals(parsed_qs['openid.ax.type.ext1'][0], 'http://axschema.org/contact/email')
-            self.assertEquals(parsed_qs['openid.ax.type.ext0'][0], 'http://axschema.org/namePerson')
-            self.assertEquals(parsed_qs['openid.ax.value.ext1.1'][0], user.email)
-            self.assertEquals(parsed_qs['openid.ax.value.ext0.1'][0], user.profile.name)
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_openid_invalid_password(self):
-
-        url = reverse('openid-provider-login')
-        user = UserFactory()
-
-        # login to the client so that we can persist session information
-        for method in ['POST', 'GET']:
-            self.client.login(username=user.username, password='test')
-            self.attempt_login(200, method)
-            openid_setup = self.client.session['openid_setup']
-            self.assertIn('post_params', openid_setup)
-            post_args = {
-                'email': user.email,
-                'password': 'bad_password',
-            }
-
-            # call url again, this time with username and password
-            resp = self.client.post(url, post_args)
-            self.assertEquals(resp.status_code, 302)
-            redirect_url = resp['Location']
-            parsed_url = urlparse(redirect_url)
-            query_params = parse_qs(parsed_url[4])
-            self.assertIn('openid.return_to', query_params)
-            self.assertTrue(
-                query_params['openid.return_to'][0].startswith('http://testserver/openid/complete/')
-            )
-
-
-class OpenIdProviderLiveServerTest(LiveServerTestCase):
-    """
-    In order for this absolute URL to work (i.e. to get xrds, then authentication)
-    in the test environment, we either need a live server that works with the default
-    fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
-    Here we do the former.
-    """
-
-    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
-                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
-                'OpenID not enabled')
-    def test_begin_login(self):
-        # the provider URL must be converted to an absolute URL in order to be
-        # used as an openid provider.
-        provider_url = reverse('openid-provider-xrds')
-        factory = RequestFactory()
-        request = factory.request()
-        abs_provider_url = request.build_absolute_uri(location=provider_url)
-
-        # In order for this absolute URL to work (i.e. to get xrds, then authentication)
-        # in the test environment, we either need a live server that works with the default
-        # fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
-        # Here we do the latter:
-        fetcher = MyFetcher(self.client)
-        openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
-
-        # now we can begin the login process by invoking a local openid client,
-        # with a pointer to the (also-local) openid provider:
-        with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
-            url = reverse('openid-login')
-            resp = self.client.post(url)
-            code = 200
-            self.assertEqual(resp.status_code, code,
-                             "got code {0} for url '{1}'. Expected code {2}"
-                             .format(resp.status_code, url, code))
-
-    @classmethod
-    def tearDownClass(cls):
-        """
-        Workaround for a runtime error that occurs
-        intermittently when the server thread doesn't shut down
-        within 2 seconds.
-
-        Since the server is running in a Django thread and will
-        be terminated when the test suite terminates,
-        this shouldn't cause a resource allocation issue.
-        """
-        try:
-            super(OpenIdProviderLiveServerTest, cls).tearDownClass()
-        except RuntimeError:
-            print "Warning: Could not shut down test server."
diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py
deleted file mode 100644
index bfd4511..0000000
--- a/common/djangoapps/external_auth/tests/test_shib.py
+++ /dev/null
@@ -1,595 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Tests for Shibboleth Authentication
-@jbau
-"""
-import unittest
-
-from ddt import ddt, data
-from django.conf import settings
-from django.http import HttpResponseRedirect
-from django.test import TestCase
-from django.test.client import RequestFactory, Client as DjangoTestClient
-from django.test.utils import override_settings
-from django.core.urlresolvers import reverse
-from django.contrib.auth.models import AnonymousUser, User
-from importlib import import_module
-from external_auth.models import ExternalAuthMap
-from external_auth.views import (
-    shib_login, course_specific_login, course_specific_register, _flatten_to_ascii
-)
-from mock import patch
-from nose.plugins.attrib import attr
-from urllib import urlencode
-
-from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
-from student.views import change_enrollment
-from student.models import UserProfile, CourseEnrollment
-from student.tests.factories import UserFactory
-from xmodule.modulestore.tests.factories import CourseFactory
-from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-from xmodule.modulestore import ModuleStoreEnum
-
-
-# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
-# attributes via request.META.  We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
-# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
-
-# For the sake of python convention we'll make all of these variable names ALL_CAPS
-# These values would all returned from request.META, so they need to be str, not unicode
-IDP = 'https://idp.stanford.edu/'
-REMOTE_USER = 'test_user@stanford.edu'
-MAILS = [None, '', 'test_user@stanford.edu']  # unicode shouldn't be in emails, would fail django's email validator
-DISPLAYNAMES = [None, '', 'Jason 包']
-GIVENNAMES = [None, '', 'jasön; John; bob']  # At Stanford, the givenNames can be a list delimited by ';'
-SNS = [None, '', '包; smith']  # At Stanford, the sns can be a list delimited by ';'
-
-
-def gen_all_identities():
-    """
-    A generator for all combinations of test inputs.
-    Each generated item is a dict that represents what a shib IDP
-    could potentially pass to django via request.META, i.e.
-    setting (or not) request.META['givenName'], etc.
-    """
-    def _build_identity_dict(mail, display_name, given_name, surname):
-        """ Helper function to return a dict of test identity """
-        meta_dict = {'Shib-Identity-Provider': IDP,
-                     'REMOTE_USER': REMOTE_USER}
-        if display_name is not None:
-            meta_dict['displayName'] = display_name
-        if mail is not None:
-            meta_dict['mail'] = mail
-        if given_name is not None:
-            meta_dict['givenName'] = given_name
-        if surname is not None:
-            meta_dict['sn'] = surname
-        return meta_dict
-
-    for mail in MAILS:
-        for given_name in GIVENNAMES:
-            for surname in SNS:
-                for display_name in DISPLAYNAMES:
-                    yield _build_identity_dict(mail, display_name, given_name, surname)
-
-
-@attr(shard=3)
-@ddt
-@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache')
-class ShibSPTest(CacheIsolationTestCase):
-    """
-    Tests for the Shibboleth SP, which communicates via request.META
-    (Apache environment variables set by mod_shib)
-    """
-
-    ENABLED_CACHES = ['default']
-
-    request_factory = RequestFactory()
-
-    def setUp(self):
-        super(ShibSPTest, self).setUp()
-        self.test_user_id = ModuleStoreEnum.UserID.test
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    def test_exception_shib_login(self):
-        """
-        Tests that we get the error page when there is no REMOTE_USER
-        or Shib-Identity-Provider in request.META
-        """
-        no_remote_user_response = self.client.get(reverse('shib-login'), HTTP_SHIB_IDENTITY_PROVIDER=IDP)
-        self.assertEqual(no_remote_user_response.status_code, 403)
-        self.assertIn("identity server did not return your ID information", no_remote_user_response.content)
-
-        no_idp_response = self.client.get(reverse('shib-login'), HTTP_REMOTE_USER=REMOTE_USER)
-        self.assertEqual(no_idp_response.status_code, 403)
-        self.assertIn("identity server did not return your ID information", no_idp_response.content)
-
-    def _assert_shib_login_is_logged(self, audit_log_call, remote_user):
-        """Asserts that shibboleth login attempt is being logged"""
-        remote_user = _flatten_to_ascii(remote_user)  # django usernames have to be ascii
-        method_name, args, _kwargs = audit_log_call
-        self.assertEquals(method_name, 'info')
-        self.assertEquals(len(args), 1)
-        self.assertIn(u'logged in via Shibboleth', args[0])
-        self.assertIn(remote_user, args[0])
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    def test_shib_login(self):
-        """
-        Tests that:
-          * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in
-          * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page
-          * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email
-            of an existing user without an existing ExternalAuthMap links the two and log the user in
-          * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email
-            of an existing user that already has an ExternalAuthMap causes an error (403)
-          * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear
-        """
-
-        user_w_map = UserFactory.create(email='withmap@stanford.edu')
-        extauth = ExternalAuthMap(external_id='withmap@stanford.edu',
-                                  external_email='',
-                                  external_domain='shib:https://idp.stanford.edu/',
-                                  external_credentials="",
-                                  user=user_w_map)
-        user_wo_map = UserFactory.create(email='womap@stanford.edu')
-        user_w_map.save()
-        user_wo_map.save()
-        extauth.save()
-
-        inactive_user = UserFactory.create(email='inactive@stanford.edu')
-        inactive_user.is_active = False
-        inactive_extauth = ExternalAuthMap(external_id='inactive@stanford.edu',
-                                           external_email='',
-                                           external_domain='shib:https://idp.stanford.edu/',
-                                           external_credentials="",
-                                           user=inactive_user)
-        inactive_user.save()
-        inactive_extauth.save()
-
-        idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/']
-        remote_users = ['withmap@stanford.edu', 'womap@stanford.edu',
-                        'testuser2@someother_idp.com', 'inactive@stanford.edu']
-
-        for idp in idps:
-            for remote_user in remote_users:
-
-                self.client.logout()
-                with patch('external_auth.views.AUDIT_LOG') as mock_audit_log:
-                    response = self.client.get(
-                        reverse('shib-login'),
-                        **{
-                            'Shib-Identity-Provider': idp,
-                            'mail': remote_user,
-                            'REMOTE_USER': remote_user,
-                        }
-                    )
-                audit_log_calls = mock_audit_log.method_calls
-
-                if idp == "https://idp.stanford.edu/" and remote_user == 'withmap@stanford.edu':
-                    self.assertRedirects(response, '/dashboard')
-                    self.assertEquals(int(self.client.session['_auth_user_id']), user_w_map.id)
-                    # verify logging:
-                    self.assertEquals(len(audit_log_calls), 2)
-                    self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
-                    method_name, args, _kwargs = audit_log_calls[1]
-                    self.assertEquals(method_name, 'info')
-                    self.assertEquals(len(args), 1)
-                    self.assertIn(u'Login success', args[0])
-                    self.assertIn(remote_user, args[0])
-                elif idp == "https://idp.stanford.edu/" and remote_user == 'inactive@stanford.edu':
-                    self.assertEqual(response.status_code, 403)
-                    self.assertIn("Account not yet activated: please look for link in your email", response.content)
-                    # verify logging:
-                    self.assertEquals(len(audit_log_calls), 2)
-                    self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
-                    method_name, args, _kwargs = audit_log_calls[1]
-                    self.assertEquals(method_name, 'warning')
-                    self.assertEquals(len(args), 1)
-                    self.assertIn(u'is not active after external login', args[0])
-                    # self.assertEquals(remote_user, args[1])
-                elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu':
-                    self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map))
-                    self.assertRedirects(response, '/dashboard')
-                    self.assertEquals(int(self.client.session['_auth_user_id']), user_wo_map.id)
-                    # verify logging:
-                    self.assertEquals(len(audit_log_calls), 2)
-                    self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
-                    method_name, args, _kwargs = audit_log_calls[1]
-                    self.assertEquals(method_name, 'info')
-                    self.assertEquals(len(args), 1)
-                    self.assertIn(u'Login success', args[0])
-                    self.assertIn(remote_user, args[0])
-                elif idp == "https://someother.idp.com/" and remote_user in \
-                            ['withmap@stanford.edu', 'womap@stanford.edu', 'inactive@stanford.edu']:
-                    self.assertEqual(response.status_code, 403)
-                    self.assertIn("You have already created an account using an external login", response.content)
-                    # no audit logging calls
-                    self.assertEquals(len(audit_log_calls), 0)
-                else:
-                    self.assertEqual(response.status_code, 200)
-                    self.assertContains(response,
-                                        (u"Preferences for {platform_name}"
-                                         .format(platform_name=settings.PLATFORM_NAME)))
-                    # no audit logging calls
-                    self.assertEquals(len(audit_log_calls), 0)
-
-    def _base_test_extauth_auto_activate_user_with_flag(self, log_user_string="inactive@stanford.edu"):
-        """
-        Tests that FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] means extauth automatically
-        linked users, activates them, and logs them in
-        """
-        inactive_user = UserFactory.create(email='inactive@stanford.edu')
-        inactive_user.is_active = False
-        inactive_user.save()
-        request = self.request_factory.get('/shib-login')
-        request.session = import_module(settings.SESSION_ENGINE).SessionStore()  # empty session
-        request.META.update({
-            'Shib-Identity-Provider': 'https://idp.stanford.edu/',
-            'REMOTE_USER': 'inactive@stanford.edu',
-            'mail': 'inactive@stanford.edu'
-        })
-
-        request.user = AnonymousUser()
-        with patch('external_auth.views.AUDIT_LOG') as mock_audit_log:
-            response = shib_login(request)
-        audit_log_calls = mock_audit_log.method_calls
-        # reload user from db, since the view function works via db side-effects
-        inactive_user = User.objects.get(id=inactive_user.id)
-        self.assertIsNotNone(ExternalAuthMap.objects.get(user=inactive_user))
-        self.assertTrue(inactive_user.is_active)
-        self.assertIsInstance(response, HttpResponseRedirect)
-        self.assertEqual(request.user, inactive_user)
-        self.assertEqual(response['Location'], '/dashboard')
-        # verify logging:
-        self.assertEquals(len(audit_log_calls), 3)
-        self._assert_shib_login_is_logged(audit_log_calls[0], log_user_string)
-        method_name, args, _kwargs = audit_log_calls[2]
-        self.assertEquals(method_name, 'info')
-        self.assertEquals(len(args), 1)
-        self.assertIn(u'Login success', args[0])
-        self.assertIn(log_user_string, args[0])
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    @patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'SQUELCH_PII_IN_LOGS': False})
-    def test_extauth_auto_activate_user_with_flag_no_squelch(self):
-        """
-        Wrapper to run base_test_extauth_auto_activate_user_with_flag with {'SQUELCH_PII_IN_LOGS': False}
-        """
-        self._base_test_extauth_auto_activate_user_with_flag(log_user_string="inactive@stanford.edu")
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    @patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'SQUELCH_PII_IN_LOGS': True})
-    def test_extauth_auto_activate_user_with_flag_squelch(self):
-        """
-        Wrapper to run base_test_extauth_auto_activate_user_with_flag with {'SQUELCH_PII_IN_LOGS': True}
-        """
-        self._base_test_extauth_auto_activate_user_with_flag(log_user_string="user.id: 1")
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    @data(*gen_all_identities())
-    def test_registration_form(self, identity):
-        """
-        Tests the registration form showing up with the proper parameters.
-
-        Uses django test client for its session support
-        """
-        client = DjangoTestClient()
-        # identity k/v pairs will show up in request.META
-        response = client.get(path='/shib-login/', data={}, follow=False, **identity)
-
-        self.assertEquals(response.status_code, 200)
-        mail_input_HTML = '<input class="" id="email" type="email" name="email"'
-        if not identity.get('mail'):
-            self.assertContains(response, mail_input_HTML)
-        else:
-            self.assertNotContains(response, mail_input_HTML)
-        sn_empty = not identity.get('sn')
-        given_name_empty = not identity.get('givenName')
-        displayname_empty = not identity.get('displayName')
-        fullname_input_html = '<input id="name" type="text" name="name"'
-        if sn_empty and given_name_empty and displayname_empty:
-            self.assertContains(response, fullname_input_html)
-        else:
-            self.assertNotContains(response, fullname_input_html)
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    @data(*gen_all_identities())
-    def test_registration_form_submit(self, identity):
-        """
-        Tests user creation after the registration form that pops is submitted.  If there is no shib
-        ExternalAuthMap in the session, then the created user should take the username and email from the
-        request.
-
-        Uses django test client for its session support
-        """
-        # First we pop the registration form
-        self.client.get(path='/shib-login/', data={}, follow=False, **identity)
-        # Then we have the user answer the registration form
-        # These are unicode because request.POST returns unicode
-        postvars = {'email': u'post_email@stanford.edu',
-                    'username': u'post_username',  # django usernames can't be unicode
-                    'password': u'post_pássword',
-                    'name': u'post_náme',
-                    'terms_of_service': u'true',
-                    'honor_code': u'true'}
-
-        with patch('student.views.AUDIT_LOG') as mock_audit_log:
-            self.client.post('/create_account', data=postvars)
-
-        mail = identity.get('mail')
-
-        # verify logging of login happening during account creation:
-        audit_log_calls = mock_audit_log.method_calls
-        self.assertEquals(len(audit_log_calls), 3)
-        method_name, args, _kwargs = audit_log_calls[0]
-        self.assertEquals(method_name, 'info')
-        self.assertEquals(len(args), 1)
-        self.assertIn(u'Login success on new account creation', args[0])
-        self.assertIn(u'post_username', args[0])
-        method_name, args, _kwargs = audit_log_calls[1]
-        self.assertEquals(method_name, 'info')
-        self.assertEquals(len(args), 2)
-        self.assertIn(u'User registered with external_auth', args[0])
-        self.assertEquals(u'post_username', args[1])
-        method_name, args, _kwargs = audit_log_calls[2]
-        self.assertEquals(method_name, 'info')
-        self.assertEquals(len(args), 3)
-        self.assertIn(u'Updated ExternalAuthMap for ', args[0])
-        self.assertEquals(u'post_username', args[1])
-        self.assertEquals(u'test_user@stanford.edu', args[2].external_id)
-
-        user = User.objects.get(id=self.client.session['_auth_user_id'])
-
-        # check that the created user has the right email, either taken from shib or user input
-        if mail:
-            self.assertEqual(user.email, mail)
-            self.assertEqual(list(User.objects.filter(email=postvars['email'])), [])
-            self.assertIsNotNone(User.objects.get(email=mail))  # get enforces only 1 such user
-        else:
-            self.assertEqual(user.email, postvars['email'])
-            self.assertEqual(list(User.objects.filter(email=mail)), [])
-            self.assertIsNotNone(User.objects.get(email=postvars['email']))  # get enforces only 1 such user
-
-        # check that the created user profile has the right name, either taken from shib or user input
-        profile = UserProfile.objects.get(user=user)
-        sn_empty = not identity.get('sn')
-        given_name_empty = not identity.get('givenName')
-        displayname_empty = not identity.get('displayName')
-
-        if displayname_empty:
-            if sn_empty and given_name_empty:
-                self.assertEqual(profile.name, postvars['name'])
-            else:
-                self.assertEqual(profile.name, self.client.session['ExternalAuthMap'].external_name)
-                self.assertNotIn(u';', profile.name)
-        else:
-            self.assertEqual(profile.name, self.client.session['ExternalAuthMap'].external_name)
-            self.assertEqual(profile.name, identity.get('displayName').decode('utf-8'))
-
-
-@ddt
-@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache')
-class ShibSPTestModifiedCourseware(ModuleStoreTestCase):
-    """
-    Tests for the Shibboleth SP which modify the courseware
-    """
-
-    ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
-
-    request_factory = RequestFactory()
-
-    def setUp(self):
-        super(ShibSPTestModifiedCourseware, self).setUp()
-        self.test_user_id = ModuleStoreEnum.UserID.test
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    @data(None, "", "shib:https://idp.stanford.edu/")
-    def test_course_specific_login_and_reg(self, domain):
-        """
-        Tests that the correct course specific login and registration urls work for shib
-        """
-        course = CourseFactory.create(
-            org='MITx',
-            number='999',
-            display_name='Robot Super Course',
-            user_id=self.test_user_id,
-        )
-
-        # Test for cases where course is found
-        # set domains
-
-        # temporarily set the branch to draft-preferred so we can update the course
-        with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
-            course.enrollment_domain = domain
-            self.store.update_item(course, self.test_user_id)
-
-        # setting location to test that GET params get passed through
-        login_request = self.request_factory.get('/course_specific_login/MITx/999/Robot_Super_Course' +
-                                                 '?course_id=MITx/999/Robot_Super_Course' +
-                                                 '&enrollment_action=enroll')
-        _reg_request = self.request_factory.get('/course_specific_register/MITx/999/Robot_Super_Course' +
-                                                '?course_id=MITx/999/course/Robot_Super_Course' +
-                                                '&enrollment_action=enroll')
-
-        login_response = course_specific_login(login_request, 'MITx/999/Robot_Super_Course')
-        reg_response = course_specific_register(login_request, 'MITx/999/Robot_Super_Course')
-
-        if domain and "shib" in domain:
-            self.assertIsInstance(login_response, HttpResponseRedirect)
-            self.assertEqual(login_response['Location'],
-                             reverse('shib-login') +
-                             '?course_id=MITx/999/Robot_Super_Course' +
-                             '&enrollment_action=enroll')
-            self.assertIsInstance(login_response, HttpResponseRedirect)
-            self.assertEqual(reg_response['Location'],
-                             reverse('shib-login') +
-                             '?course_id=MITx/999/Robot_Super_Course' +
-                             '&enrollment_action=enroll')
-        else:
-            self.assertIsInstance(login_response, HttpResponseRedirect)
-            self.assertEqual(login_response['Location'],
-                             reverse('signin_user') +
-                             '?course_id=MITx/999/Robot_Super_Course' +
-                             '&enrollment_action=enroll')
-            self.assertIsInstance(login_response, HttpResponseRedirect)
-            self.assertEqual(reg_response['Location'],
-                             reverse('register_user') +
-                             '?course_id=MITx/999/Robot_Super_Course' +
-                             '&enrollment_action=enroll')
-
-        # Now test for non-existent course
-        # setting location to test that GET params get passed through
-        login_request = self.request_factory.get('/course_specific_login/DNE/DNE/DNE' +
-                                                 '?course_id=DNE/DNE/DNE' +
-                                                 '&enrollment_action=enroll')
-        _reg_request = self.request_factory.get('/course_specific_register/DNE/DNE/DNE' +
-                                                '?course_id=DNE/DNE/DNE/Robot_Super_Course' +
-                                                '&enrollment_action=enroll')
-
-        login_response = course_specific_login(login_request, 'DNE/DNE/DNE')
-        reg_response = course_specific_register(login_request, 'DNE/DNE/DNE')
-
-        self.assertIsInstance(login_response, HttpResponseRedirect)
-        self.assertEqual(login_response['Location'],
-                         reverse('signin_user') +
-                         '?course_id=DNE/DNE/DNE' +
-                         '&enrollment_action=enroll')
-        self.assertIsInstance(login_response, HttpResponseRedirect)
-        self.assertEqual(reg_response['Location'],
-                         reverse('register_user') +
-                         '?course_id=DNE/DNE/DNE' +
-                         '&enrollment_action=enroll')
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    def test_enrollment_limit_by_domain(self):
-        """
-            Tests that the enrollmentDomain setting is properly limiting enrollment to those who have
-            the proper external auth
-        """
-
-        # create 2 course, one with limited enrollment one without
-        shib_course = CourseFactory.create(
-            org='Stanford',
-            number='123',
-            display_name='Shib Only',
-            enrollment_domain='shib:https://idp.stanford.edu/',
-            user_id=self.test_user_id,
-        )
-
-        open_enroll_course = CourseFactory.create(
-            org='MITx',
-            number='999',
-            display_name='Robot Super Course',
-            enrollment_domain='',
-            user_id=self.test_user_id,
-        )
-
-        # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth
-        shib_student = UserFactory.create()
-        shib_student.save()
-        extauth = ExternalAuthMap(external_id='testuser@stanford.edu',
-                                  external_email='',
-                                  external_domain='shib:https://idp.stanford.edu/',
-                                  external_credentials="",
-                                  user=shib_student)
-        extauth.save()
-
-        other_ext_student = UserFactory.create()
-        other_ext_student.username = "teststudent2"
-        other_ext_student.email = "teststudent2@other.edu"
-        other_ext_student.save()
-        extauth = ExternalAuthMap(external_id='testuser1@other.edu',
-                                  external_email='',
-                                  external_domain='shib:https://other.edu/',
-                                  external_credentials="",
-                                  user=other_ext_student)
-        extauth.save()
-
-        int_student = UserFactory.create()
-        int_student.username = "teststudent3"
-        int_student.email = "teststudent3@gmail.com"
-        int_student.save()
-
-        # Tests the two case for courses, limited and not
-        for course in [shib_course, open_enroll_course]:
-            for student in [shib_student, other_ext_student, int_student]:
-                request = self.request_factory.post('/change_enrollment')
-
-                request.POST.update({'enrollment_action': 'enroll',
-                                     'course_id': course.id.to_deprecated_string()})
-                request.user = student
-                response = change_enrollment(request)
-                # If course is not limited or student has correct shib extauth then enrollment should be allowed
-                if course is open_enroll_course or student is shib_student:
-                    self.assertEqual(response.status_code, 200)
-                    self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
-                else:
-                    self.assertEqual(response.status_code, 400)
-                    self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
-
-    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
-    def test_shib_login_enrollment(self):
-        """
-            A functionality test that a student with an existing shib login
-            can auto-enroll in a class with GET or POST params.  Also tests the direction functionality of
-            the 'next' GET/POST param
-        """
-        student = UserFactory.create()
-        extauth = ExternalAuthMap(external_id='testuser@stanford.edu',
-                                  external_email='',
-                                  external_domain='shib:https://idp.stanford.edu/',
-                                  external_credentials="",
-                                  internal_password="password",
-                                  user=student)
-        student.set_password("password")
-        student.save()
-        extauth.save()
-
-        course = CourseFactory.create(
-            org='Stanford',
-            number='123',
-            display_name='Shib Only',
-            enrollment_domain='shib:https://idp.stanford.edu/',
-            user_id=self.test_user_id,
-        )
-
-        # use django test client for sessions and url processing
-        # no enrollment before trying
-        self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
-        self.client.logout()
-        params = [
-            ('course_id', course.id.to_deprecated_string()),
-            ('enrollment_action', 'enroll'),
-            ('next', '/testredirect')
-        ]
-        request_kwargs = {'path': '/shib-login/',
-                          'data': dict(params),
-                          'follow': False,
-                          'REMOTE_USER': 'testuser@stanford.edu',
-                          'Shib-Identity-Provider': 'https://idp.stanford.edu/'}
-        response = self.client.get(**request_kwargs)
-        # successful login is a redirect to the URL that handles auto-enrollment
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], 'http://testserver/account/finish_auth?{}'.format(urlencode(params)))
-
-
-class ShibUtilFnTest(TestCase):
-    """
-    Tests util functions in shib module
-    """
-    def test__flatten_to_ascii(self):
-        DIACRITIC = u"àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ"  # pylint: disable=invalid-name
-        STR_DIACRI = "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ"  # pylint: disable=invalid-name
-        FLATTENED = u"aeiouAEIOUaeiouyAEIOUYaeiouAEIOUanoANOaeiouyAEIOUYaAcC"  # pylint: disable=invalid-name
-        self.assertEqual(_flatten_to_ascii('jasön'), 'jason')  # umlaut
-        self.assertEqual(_flatten_to_ascii('Jason包'), 'Jason')  # mandarin, so it just gets dropped
-        self.assertEqual(_flatten_to_ascii('abc'), 'abc')  # pass through
-
-        unicode_test = _flatten_to_ascii(DIACRITIC)
-        self.assertEqual(unicode_test, FLATTENED)
-        self.assertIsInstance(unicode_test, unicode)
-
-        str_test = _flatten_to_ascii(STR_DIACRI)
-        self.assertEqual(str_test, FLATTENED)
-        self.assertIsInstance(str_test, str)
diff --git a/common/djangoapps/external_auth/tests/test_ssl.py b/common/djangoapps/external_auth/tests/test_ssl.py
deleted file mode 100644
index 55cfc84..0000000
--- a/common/djangoapps/external_auth/tests/test_ssl.py
+++ /dev/null
@@ -1,417 +0,0 @@
-"""
-Provides unit tests for SSL based authentication portions
-of the external_auth app.
-"""
-import copy
-import unittest
-
-from contextlib import contextmanager
-from django.conf import settings
-from django.contrib.auth import SESSION_KEY
-from django.contrib.auth.models import AnonymousUser, User
-from django.contrib.sessions.middleware import SessionMiddleware
-from django.core.urlresolvers import reverse
-from django.test.client import Client
-from django.test.client import RequestFactory
-from django.test.utils import override_settings
-from external_auth.models import ExternalAuthMap
-import external_auth.views
-from mock import Mock, patch
-
-from student.models import CourseEnrollment
-from student.roles import CourseStaffRole
-from student.tests.factories import UserFactory
-from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-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
-CACHES_ENABLE_GENERAL = copy.deepcopy(settings.CACHES)
-CACHES_ENABLE_GENERAL['general']['BACKEND'] = 'django.core.cache.backends.locmem.LocMemCache'
-
-
-@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH)
-@override_settings(CACHES=CACHES_ENABLE_GENERAL)
-class SSLClientTest(ModuleStoreTestCase):
-    """
-    Tests SSL Authentication code sections of external_auth
-    """
-
-    AUTH_DN = '/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}'
-    USER_NAME = 'test_user_ssl'
-    USER_EMAIL = 'test_user_ssl@EDX.ORG'
-    MOCK_URL = '/'
-
-    @contextmanager
-    def _create_ssl_request(self, url):
-        """Creates a basic request for SSL use."""
-        request = self.factory.get(url)
-        request.META['SSL_CLIENT_S_DN'] = self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
-        request.user = AnonymousUser()
-        middleware = SessionMiddleware()
-        middleware.process_request(request)
-        request.session.save()
-
-        with patch('edxmako.request_context.get_current_request', return_value=request):
-            yield request
-
-    @contextmanager
-    def _create_normal_request(self, url):
-        """Creates sessioned request without SSL headers"""
-        request = self.factory.get(url)
-        request.user = AnonymousUser()
-        middleware = SessionMiddleware()
-        middleware.process_request(request)
-        request.session.save()
-
-        with patch('edxmako.request_context.get_current_request', return_value=request):
-            yield request
-
-    def setUp(self):
-        """Setup test case by adding primary user."""
-        super(SSLClientTest, self).setUp()
-        self.client = Client()
-        self.factory = RequestFactory()
-        self.mock = Mock()
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    def test_ssl_login_with_signup_lms(self):
-        """
-        Validate that an SSL login creates an eamap user and
-        redirects them to the signup page.
-        """
-        with self._create_ssl_request('/') as request:
-            response = external_auth.views.ssl_login(request)
-
-        # Response should contain template for signup form, eamap should have user, and internal
-        # auth should not have a user
-        self.assertIn('<form role="form" id="register-form" method="post"', response.content)
-        try:
-            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
-
-        with self.assertRaises(User.DoesNotExist):
-            User.objects.get(email=self.USER_EMAIL)
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
-    def test_ssl_login_with_signup_cms(self):
-        """
-        Validate that an SSL login creates an eamap user and
-        redirects them to the signup page on CMS.
-        """
-        self.client.get(
-            reverse('contentstore.views.login_page'),
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
-        )
-
-        try:
-            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
-
-        with self.assertRaises(User.DoesNotExist):
-            User.objects.get(email=self.USER_EMAIL)
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_ssl_login_without_signup_lms(self):
-        """
-        Test IMMEDIATE_SIGNUP feature flag and ensure the user account is automatically created
-        and the user is redirected to slash.
-        """
-        with self._create_ssl_request('/') as request:
-            external_auth.views.ssl_login(request)
-
-        # Assert our user exists in both eamap and Users, and that we are logged in
-        try:
-            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
-        try:
-            User.objects.get(email=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to internal users, exception was {0}'.format(str(ex)))
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_ssl_login_without_signup_cms(self):
-        """
-        Test IMMEDIATE_SIGNUP feature flag and ensure the user account is
-        automatically created on CMS, and that we are redirected
-        to courses.
-        """
-
-        response = self.client.get(
-            reverse('contentstore.views.login_page'),
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
-        )
-        self.assertEqual(response.status_code, 302)
-        self.assertIn('/course', response['location'])
-
-        # Assert our user exists in both eamap and Users, and that we are logged in
-        try:
-            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
-        try:
-            User.objects.get(email=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to internal users, exception was {0}'.format(str(ex)))
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_default_login_decorator_ssl(self):
-        """
-        Make sure that SSL login happens if it is enabled on protected
-        views instead of showing the login form.
-        """
-        response = self.client.get(reverse('dashboard'), follows=True)
-        self.assertEqual(response.status_code, 302)
-        self.assertIn(reverse('signin_user'), response['location'])
-
-        response = self.client.get(
-            reverse('dashboard'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
-        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')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_registration_page_bypass(self):
-        """
-        This tests to make sure when immediate signup is on that
-        the user doesn't get presented with the registration page.
-        """
-        response = self.client.get(
-            reverse('register_user'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
-        self.assertEquals(('http://testserver/dashboard', 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_IMMEDIATE_SIGNUP)
-    def test_cms_registration_page_bypass(self):
-        """
-        This tests to make sure when immediate signup is on that
-        the user doesn't get presented with the registration page.
-        """
-        response = self.client.get(
-            reverse('signup'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
-        )
-        self.assertEqual(response.status_code, 404)
-        # assert that we are logged in
-        self.assertIn(SESSION_KEY, self.client.session)
-
-        # Now that we are logged in, make sure we don't see the registration page
-        response = self.client.get(reverse('signup'), follow=True)
-        self.assertEqual(response.status_code, 404)
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_signin_page_bypass(self):
-        """
-        This tests to make sure when ssl authentication is on
-        that user doesn't get presented with the login page if they
-        have a certificate.
-        """
-        # Test that they do signin if they don't have a cert
-        response = self.client.get(reverse('signin_user'))
-        self.assertEqual(200, response.status_code)
-        self.assertIn('login-and-registration-container', response.content)
-
-        # And get directly logged in otherwise
-        response = self.client.get(
-            reverse('signin_user'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
-        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')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_ssl_bad_eamap(self):
-        """
-        This tests the response when a user exists but their eamap
-        password doesn't match their internal password.
-
-        The internal password use for certificates has been removed
-        and this should not fail.
-        """
-        # Create account, break internal password, and activate account
-
-        with self._create_ssl_request('/') as request:
-            external_auth.views.ssl_login(request)
-        user = User.objects.get(email=self.USER_EMAIL)
-        user.set_password('not autogenerated')
-        user.is_active = True
-        user.save()
-
-        # Make sure we can still login
-        self.client.get(
-            reverse('signin_user'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
-        self.assertIn(SESSION_KEY, self.client.session)
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @override_settings(FEATURES=FEATURES_WITHOUT_SSL_AUTH)
-    def test_ssl_decorator_no_certs(self):
-        """Make sure no external auth happens without SSL enabled"""
-
-        dec_mock = external_auth.views.ssl_login_shortcut(self.mock)
-
-        with self._create_normal_request(self.MOCK_URL) as request:
-            request.user = AnonymousUser()
-            # Call decorated mock function to make sure it passes
-            # the call through without hitting the external_auth functions and
-            # thereby creating an external auth map object.
-            dec_mock(request)
-        self.assertTrue(self.mock.called)
-        self.assertEqual(0, len(ExternalAuthMap.objects.all()))
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    def test_ssl_login_decorator(self):
-        """Create mock function to test ssl login decorator"""
-
-        dec_mock = external_auth.views.ssl_login_shortcut(self.mock)
-
-        # Test that anonymous without cert doesn't create authmap
-        with self._create_normal_request(self.MOCK_URL) as request:
-            dec_mock(request)
-        self.assertTrue(self.mock.called)
-        self.assertEqual(0, len(ExternalAuthMap.objects.all()))
-
-        # Test valid user
-        self.mock.reset_mock()
-        with self._create_ssl_request(self.MOCK_URL) as request:
-            dec_mock(request)
-        self.assertFalse(self.mock.called)
-        self.assertEqual(1, len(ExternalAuthMap.objects.all()))
-
-        # Test logged in user gets called
-        self.mock.reset_mock()
-        with self._create_ssl_request(self.MOCK_URL) as request:
-            request.user = UserFactory()
-            dec_mock(request)
-        self.assertTrue(self.mock.called)
-
-    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
-    def test_ssl_decorator_auto_signup(self):
-        """
-        Test that with auto signup the decorator
-        will bypass registration and call retfun.
-        """
-
-        dec_mock = external_auth.views.ssl_login_shortcut(self.mock)
-        with self._create_ssl_request(self.MOCK_URL) as request:
-            dec_mock(request)
-
-        # Assert our user exists in both eamap and Users
-        try:
-            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
-        try:
-            User.objects.get(email=self.USER_EMAIL)
-        except ExternalAuthMap.DoesNotExist, ex:
-            self.fail('User did not get properly added to internal users, exception was {0}'.format(str(ex)))
-        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)
-    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'
-        )
-
-        with self._create_ssl_request('/') as request:
-            external_auth.views.ssl_login(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.assertNotIn(SESSION_KEY, 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'
-        )
-
-        with self._create_ssl_request('/') as request:
-            external_auth.views.ssl_login(request)
-        user = User.objects.get(email=self.USER_EMAIL)
-        CourseEnrollment.enroll(user, course.id)
-
-        CourseStaffRole(course.id).add_users(user)
-        course_private_url = reverse('course_handler', args=(unicode(course.id),))
-        self.assertNotIn(SESSION_KEY, 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 == 'lms.urls', 'Test only valid in lms')
-    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE)
-    def test_ssl_logout(self):
-        """
-        Because the branding view is cached for anonymous users and we
-        use that to login users, the browser wasn't actually making the
-        request to that view as the redirect was being cached. This caused
-        a redirect loop, and this test confirms that that won't happen.
-
-        Test is only in LMS because we don't use / in studio to login SSL users.
-        """
-        response = self.client.get(
-            reverse('dashboard'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
-        self.assertEquals(('http://testserver/dashboard', 302),
-                          response.redirect_chain[-1])
-        self.assertIn(SESSION_KEY, self.client.session)
-        response = self.client.get(
-            reverse('logout'), follow=True,
-            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
-        )
-        # Make sure that even though we logged out, we have logged back in
-        self.assertIn(SESSION_KEY, self.client.session)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
deleted file mode 100644
index 795741a..0000000
--- a/common/djangoapps/external_auth/views.py
+++ /dev/null
@@ -1,947 +0,0 @@
-import functools
-import json
-import logging
-import random
-import re
-import string
-import fnmatch
-import unicodedata
-import urllib
-
-from textwrap import dedent
-from external_auth.models import ExternalAuthMap
-from external_auth.djangostore import DjangoOpenIDStore
-
-from django.conf import settings
-from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
-from django.contrib.auth.models import User
-from django.core.urlresolvers import reverse
-from django.core.validators import validate_email
-from django.core.exceptions import ValidationError
-
-if settings.FEATURES.get('AUTH_USE_CAS'):
-    from django_cas.views import login as django_cas_login
-
-from student.helpers import get_next_url_for_login_page
-from student.models import UserProfile
-
-from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
-from django.utils.http import urlquote, is_safe_url
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-
-from edxmako.shortcuts import render_to_response, render_to_string
-try:
-    from django.views.decorators.csrf import csrf_exempt
-except ImportError:
-    from django.contrib.csrf.middleware import csrf_exempt
-from django.views.decorators.csrf import ensure_csrf_cookie
-
-import django_openid_auth.views as openid_views
-from django_openid_auth import auth as openid_auth
-from openid.consumer.consumer import SUCCESS
-
-from openid.server.server import Server, ProtocolError, UntrustedReturnURL
-from openid.server.trustroot import TrustRoot
-from openid.extensions import ax, sreg
-from ratelimitbackend.exceptions import RateLimitException
-
-import student.views
-from xmodule.modulestore.django import modulestore
-from opaque_keys.edx.locations import SlashSeparatedCourseKey
-
-log = logging.getLogger("edx.external_auth")
-AUDIT_LOG = logging.getLogger("audit")
-
-SHIBBOLETH_DOMAIN_PREFIX = settings.SHIBBOLETH_DOMAIN_PREFIX
-OPENID_DOMAIN_PREFIX = settings.OPENID_DOMAIN_PREFIX
-
-# -----------------------------------------------------------------------------
-# OpenID Common
-# -----------------------------------------------------------------------------
-
-
-@csrf_exempt
-def default_render_failure(request,
-                           message,
-                           status=403,
-                           template_name='extauth_failure.html',
-                           exception=None):
-    """Render an Openid error page to the user"""
-
-    log.debug("In openid_failure " + message)
-
-    data = render_to_string(template_name,
-                            dict(message=message, exception=exception))
-
-    return HttpResponse(data, status=status)
-
-
-# -----------------------------------------------------------------------------
-# OpenID Authentication
-# -----------------------------------------------------------------------------
-
-
-def generate_password(length=12, chars=string.letters + string.digits):
-    """Generate internal password for externally authenticated user"""
-    choice = random.SystemRandom().choice
-    return ''.join([choice(chars) for _i in range(length)])
-
-
-@csrf_exempt
-def openid_login_complete(request,
-                          redirect_field_name=REDIRECT_FIELD_NAME,
-                          render_failure=None):
-    """Complete the openid login process"""
-
-    render_failure = (render_failure or default_render_failure)
-
-    openid_response = openid_views.parse_openid_response(request)
-    if not openid_response:
-        return render_failure(request,
-                              'This is an OpenID relying party endpoint.')
-
-    if openid_response.status == SUCCESS:
-        external_id = openid_response.identity_url
-        oid_backend = openid_auth.OpenIDBackend()
-        details = oid_backend._extract_user_details(openid_response)
-
-        log.debug('openid success, details=%s', details)
-
-        url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
-        external_domain = "{0}{1}".format(OPENID_DOMAIN_PREFIX, url)
-        fullname = '%s %s' % (details.get('first_name', ''),
-                              details.get('last_name', ''))
-
-        return _external_login_or_signup(
-            request,
-            external_id,
-            external_domain,
-            details,
-            details.get('email', ''),
-            fullname,
-            retfun=functools.partial(redirect, get_next_url_for_login_page(request)),
-        )
-
-    return render_failure(request, 'Openid failure')
-
-
-def _external_login_or_signup(request,
-                              external_id,
-                              external_domain,
-                              credentials,
-                              email,
-                              fullname,
-                              retfun=None):
-    """Generic external auth login or signup"""
-    # see if we have a map from this external_id to an edX username
-    try:
-        eamap = ExternalAuthMap.objects.get(external_id=external_id,
-                                            external_domain=external_domain)
-        log.debug(u'Found eamap=%s', eamap)
-    except ExternalAuthMap.DoesNotExist:
-        # go render form for creating edX user
-        eamap = ExternalAuthMap(external_id=external_id,
-                                external_domain=external_domain,
-                                external_credentials=json.dumps(credentials))
-        eamap.external_email = email
-        eamap.external_name = fullname
-        eamap.internal_password = generate_password()
-        log.debug(u'Created eamap=%s', eamap)
-        eamap.save()
-
-    log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname)
-    uses_shibboleth = settings.FEATURES.get('AUTH_USE_SHIB') and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)
-    uses_certs = settings.FEATURES.get('AUTH_USE_CERTIFICATES')
-    internal_user = eamap.user
-    if internal_user is None:
-        if uses_shibboleth:
-            # If we are using shib, try to link accounts
-            # For Stanford shib, the email the idp returns is actually under the control of the user.
-            # Since the id the idps return is not user-editable, and is of the from "username@stanford.edu",
-            # use the id to link accounts instead.
-            try:
-                link_user = User.objects.get(email=eamap.external_id)
-                if not ExternalAuthMap.objects.filter(user=link_user).exists():
-                    # if there's no pre-existing linked eamap, we link the user
-                    eamap.user = link_user
-                    eamap.save()
-                    internal_user = link_user
-                    log.info(u'SHIB: Linking existing account for %s', eamap.external_id)
-                    # now pass through to log in
-                else:
-                    # otherwise, there must have been an error, b/c we've already linked a user with these external
-                    # creds
-                    failure_msg = _(
-                        "You have already created an account using "
-                        "an external login like WebAuth or Shibboleth. "
-                        "Please contact {tech_support_email} for support."
-                    ).format(
-                        tech_support_email=settings.TECH_SUPPORT_EMAIL,
-                    )
-                    return default_render_failure(request, failure_msg)
-            except User.DoesNotExist:
-                log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email)
-                return _signup(request, eamap, retfun)
-        else:
-            log.info(u'No user for %s yet. doing signup', eamap.external_email)
-            return _signup(request, eamap, retfun)
-
-    # We trust shib's authentication, so no need to authenticate using the password again
-    uname = internal_user.username
-    if uses_shibboleth:
-        user = internal_user
-        # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe
-        if settings.AUTHENTICATION_BACKENDS:
-            auth_backend = settings.AUTHENTICATION_BACKENDS[0]
-        else:
-            auth_backend = 'ratelimitbackend.backends.RateLimitModelBackend'
-        user.backend = auth_backend
-        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-            AUDIT_LOG.info(u'Linked user.id: {0} logged in via Shibboleth'.format(user.id))
-        else:
-            AUDIT_LOG.info(u'Linked user "{0}" logged in via Shibboleth'.format(user.email))
-    elif uses_certs:
-        # Certificates are trusted, so just link the user and log the action
-        user = internal_user
-        user.backend = 'ratelimitbackend.backends.RateLimitModelBackend'
-        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-            AUDIT_LOG.info(u'Linked user_id {0} logged in via SSL certificate'.format(user.id))
-        else:
-            AUDIT_LOG.info(u'Linked user "{0}" logged in via SSL certificate'.format(user.email))
-    else:
-        user = authenticate(username=uname, password=eamap.internal_password, request=request)
-    if user is None:
-        # we want to log the failure, but don't want to log the password attempted:
-        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-            AUDIT_LOG.warning(u'External Auth Login failed')
-        else:
-            AUDIT_LOG.warning(u'External Auth Login failed for "{0}"'.format(uname))
-        return _signup(request, eamap, retfun)
-
-    if not user.is_active:
-        if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
-            # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users
-            # that aren't already active
-            user.is_active = True
-            user.save()
-            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-                AUDIT_LOG.info(u'Activating user {0} due to external auth'.format(user.id))
-            else:
-                AUDIT_LOG.info(u'Activating user "{0}" due to external auth'.format(uname))
-        else:
-            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-                AUDIT_LOG.warning(u'User {0} is not active after external login'.format(user.id))
-            else:
-                AUDIT_LOG.warning(u'User "{0}" is not active after external login'.format(uname))
-            # TODO: improve error page
-            msg = 'Account not yet activated: please look for link in your email'
-            return default_render_failure(request, msg)
-
-    login(request, user)
-    request.session.set_expiry(0)
-
-    if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-        AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id))
-    else:
-        AUDIT_LOG.info(u"Login success - {0} ({1})".format(user.username, user.email))
-    if retfun is None:
-        return redirect('/')
-    return retfun()
-
-
-def _flatten_to_ascii(txt):
-    """
-    Flattens possibly unicode txt to ascii (django username limitation)
-    @param name:
-    @return: the flattened txt (in the same type as was originally passed in)
-    """
-    if isinstance(txt, str):
-        txt = txt.decode('utf-8')
-        return unicodedata.normalize('NFKD', txt).encode('ASCII', 'ignore')
-    else:
-        return unicode(unicodedata.normalize('NFKD', txt).encode('ASCII', 'ignore'))
-
-
-@ensure_csrf_cookie
-def _signup(request, eamap, retfun=None):
-    """
-    Present form to complete for signup via external authentication.
-    Even though the user has external credentials, he/she still needs
-    to create an account on the edX system, and fill in the user
-    registration form.
-
-    eamap is an ExternalAuthMap object, specifying the external user
-    for which to complete the signup.
-
-    retfun is a function to execute for the return value, if immediate
-    signup is used.  That allows @ssl_login_shortcut() to work.
-    """
-    # save this for use by student.views.create_account
-    request.session['ExternalAuthMap'] = eamap
-
-    if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP', ''):
-        # do signin immediately, by calling create_account, instead of asking
-        # student to fill in form.  MIT students already have information filed.
-        username = eamap.external_email.split('@', 1)[0]
-        username = username.replace('.', '_')
-        post_vars = dict(username=username,
-                         honor_code=u'true',
-                         terms_of_service=u'true')
-        log.info(u'doing immediate signup for %s, params=%s', username, post_vars)
-        student.views.create_account(request, post_vars)
-        # should check return content for successful completion before
-        if retfun is not None:
-            return retfun()
-        else:
-            return redirect('/')
-
-    # default conjoin name, no spaces, flattened to ascii b/c django can't handle unicode usernames, sadly
-    # but this only affects username, not fullname
-    username = re.sub(r'\s', '', _flatten_to_ascii(eamap.external_name), flags=re.UNICODE)
-
-    context = {'has_extauth_info': True,
-               'show_signup_immediately': True,
-               'extauth_domain': eamap.external_domain,
-               'extauth_id': eamap.external_id,
-               'extauth_email': eamap.external_email,
-               'extauth_username': username,
-               'extauth_name': eamap.external_name,
-               'ask_for_tos': True,
-               }
-
-    # Some openEdX instances can't have terms of service for shib users, like
-    # according to Stanford's Office of General Counsel
-    uses_shibboleth = (settings.FEATURES.get('AUTH_USE_SHIB') and
-                       eamap.external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX))
-    if uses_shibboleth and settings.FEATURES.get('SHIB_DISABLE_TOS'):
-        context['ask_for_tos'] = False
-
-    # detect if full name is blank and ask for it from user
-    context['ask_for_fullname'] = eamap.external_name.strip() == ''
-
-    # validate provided mail and if it's not valid ask the user
-    try:
-        validate_email(eamap.external_email)
-        context['ask_for_email'] = False
-    except ValidationError:
-        context['ask_for_email'] = True
-
-    log.info(u'EXTAUTH: Doing signup for %s', eamap.external_id)
-
-    return student.views.register_user(request, extra_context=context)
-
-
-# -----------------------------------------------------------------------------
-# MIT SSL
-# -----------------------------------------------------------------------------
-
-
-def _ssl_dn_extract_info(dn_string):
-    """
-    Extract username, email address (may be anyuser@anydomain.com) and
-    full name from the SSL DN string.  Return (user,email,fullname) if
-    successful, and None otherwise.
-    """
-    ss = re.search('/emailAddress=(.*)@([^/]+)', dn_string)
-    if ss:
-        user = ss.group(1)
-        email = "%s@%s" % (user, ss.group(2))
-    else:
-        return None
-    ss = re.search('/CN=([^/]+)/', dn_string)
-    if ss:
-        fullname = ss.group(1)
-    else:
-        return None
-    return (user, email, fullname)
-
-
-def ssl_get_cert_from_request(request):
-    """
-    Extract user information from certificate, if it exists, returning (user, email, fullname).
-    Else return None.
-    """
-    certkey = "SSL_CLIENT_S_DN"  # specify the request.META field to use
-
-    cert = request.META.get(certkey, '')
-    if not cert:
-        cert = request.META.get('HTTP_' + certkey, '')
-    if not cert:
-        try:
-            # try the direct apache2 SSL key
-            cert = request._req.subprocess_env.get(certkey, '')
-        except Exception:
-            return ''
-
-    return cert
-
-
-def ssl_login_shortcut(fn):
-    """
-    Python function decorator for login procedures, to allow direct login
-    based on existing ExternalAuth record and MIT ssl certificate.
-    """
-    def wrapped(*args, **kwargs):
-        """
-        This manages the function wrapping, by determining whether to inject
-        the _external signup or just continuing to the internal function
-        call.
-        """
-
-        if not settings.FEATURES['AUTH_USE_CERTIFICATES']:
-            return fn(*args, **kwargs)
-        request = args[0]
-
-        if request.user and request.user.is_authenticated():  # don't re-authenticate
-            return fn(*args, **kwargs)
-
-        cert = ssl_get_cert_from_request(request)
-        if not cert:		# no certificate information - show normal login window
-            return fn(*args, **kwargs)
-
-        def retfun():
-            """Wrap function again for call by _external_login_or_signup"""
-            return fn(*args, **kwargs)
-
-        (_user, email, fullname) = _ssl_dn_extract_info(cert)
-        return _external_login_or_signup(
-            request,
-            external_id=email,
-            external_domain="ssl:MIT",
-            credentials=cert,
-            email=email,
-            fullname=fullname,
-            retfun=retfun
-        )
-    return wrapped
-
-
-@csrf_exempt
-def ssl_login(request):
-    """
-    This is called by branding.views.index when
-    FEATURES['AUTH_USE_CERTIFICATES'] = True
-
-    Used for MIT user authentication.  This presumes the web server
-    (nginx) has been configured to require specific client
-    certificates.
-
-    If the incoming protocol is HTTPS (SSL) then authenticate via
-    client certificate.  The certificate provides user email and
-    fullname; this populates the ExternalAuthMap.  The user is
-    nevertheless still asked to complete the edX signup.
-
-    Else continues on with student.views.index, and no authentication.
-    """
-    # Just to make sure we're calling this only at MIT:
-    if not settings.FEATURES['AUTH_USE_CERTIFICATES']:
-        return HttpResponseForbidden()
-
-    cert = ssl_get_cert_from_request(request)
-
-    if not cert:
-        # no certificate information - go onward to main index
-        return student.views.index(request)
-
-    (_user, email, fullname) = _ssl_dn_extract_info(cert)
-
-    redirect_to = get_next_url_for_login_page(request)
-    retfun = functools.partial(redirect, redirect_to)
-    return _external_login_or_signup(
-        request,
-        external_id=email,
-        external_domain="ssl:MIT",
-        credentials=cert,
-        email=email,
-        fullname=fullname,
-        retfun=retfun
-    )
-
-
-# -----------------------------------------------------------------------------
-# CAS (Central Authentication Service)
-# -----------------------------------------------------------------------------
-def cas_login(request, next_page=None, required=False):
-    """
-        Uses django_cas for authentication.
-        CAS is a common authentcation method pioneered by Yale.
-        See http://en.wikipedia.org/wiki/Central_Authentication_Service
-
-        Does normal CAS login then generates user_profile if nonexistent,
-        and if login was successful.  We assume that user details are
-        maintained by the central service, and thus an empty user profile
-        is appropriate.
-    """
-
-    ret = django_cas_login(request, next_page, required)
-
-    if request.user.is_authenticated():
-        user = request.user
-        UserProfile.objects.get_or_create(
-            user=user,
-            defaults={'name': user.username}
-        )
-
-    return ret
-
-
-# -----------------------------------------------------------------------------
-# Shibboleth (Stanford and others.  Uses *Apache* environment variables)
-# -----------------------------------------------------------------------------
-def shib_login(request):
-    """
-        Uses Apache's REMOTE_USER environment variable as the external id.
-        This in turn typically uses EduPersonPrincipalName
-        http://www.incommonfederation.org/attributesummary.html#eduPersonPrincipal
-        but the configuration is in the shibboleth software.
-    """
-    shib_error_msg = dedent(_(
-        """
-        Your university identity server did not return your ID information to us.
-        Please try logging in again.  (You may need to restart your browser.)
-        """))
-
-    if not request.META.get('REMOTE_USER'):
-        log.error(u"SHIB: no REMOTE_USER found in request.META")
-        return default_render_failure(request, shib_error_msg)
-    elif not request.META.get('Shib-Identity-Provider'):
-        log.error(u"SHIB: no Shib-Identity-Provider in request.META")
-        return default_render_failure(request, shib_error_msg)
-    else:
-        # If we get here, the user has authenticated properly
-        shib = {attr: request.META.get(attr, '').decode('utf-8')
-                for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider', 'displayName']}
-
-        # Clean up first name, last name, and email address
-        # TODO: Make this less hardcoded re: format, but split will work
-        # even if ";" is not present, since we are accessing 1st element
-        shib['sn'] = shib['sn'].split(";")[0].strip().capitalize()
-        shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize()
-
-    # TODO: should we be logging creds here, at info level?
-    log.info(u"SHIB creds returned: %r", shib)
-
-    fullname = shib['displayName'] if shib['displayName'] else u'%s %s' % (shib['givenName'], shib['sn'])
-
-    redirect_to = get_next_url_for_login_page(request)
-    retfun = functools.partial(_safe_postlogin_redirect, redirect_to, request.get_host())
-
-    return _external_login_or_signup(
-        request,
-        external_id=shib['REMOTE_USER'],
-        external_domain=SHIBBOLETH_DOMAIN_PREFIX + shib['Shib-Identity-Provider'],
-        credentials=shib,
-        email=shib['mail'],
-        fullname=fullname,
-        retfun=retfun
-    )
-
-
-def _safe_postlogin_redirect(redirect_to, safehost, default_redirect='/'):
-    """
-    If redirect_to param is safe (not off this host), then perform the redirect.
-    Otherwise just redirect to '/'.
-    Basically copied from django.contrib.auth.views.login
-    @param redirect_to: user-supplied redirect url
-    @param safehost: which host is safe to redirect to
-    @return: an HttpResponseRedirect
-    """
-    if is_safe_url(url=redirect_to, host=safehost):
-        return redirect(redirect_to)
-    return redirect(default_redirect)
-
-
-def course_specific_login(request, course_id):
-    """
-       Dispatcher function for selecting the specific login method
-       required by the course
-    """
-    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
-    course = modulestore().get_course(course_key)
-    if not course:
-        # couldn't find the course, will just return vanilla signin page
-        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 and
-        course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)
-    ):
-        return redirect_with_get('shib-login', request.GET)
-
-    # Default fallthrough to normal signin page
-    return redirect_with_get('signin_user', request.GET)
-
-
-def course_specific_register(request, course_id):
-    """
-        Dispatcher function for selecting the specific registration method
-        required by the course
-    """
-    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
-    course = modulestore().get_course(course_key)
-
-    if not course:
-        # couldn't find the course, will just return vanilla registration page
-        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 and
-        course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)
-    ):
-        # shib-login takes care of both registration and login flows
-        return redirect_with_get('shib-login', request.GET)
-
-    # Default fallthrough to normal registration page
-    return redirect_with_get('register_user', request.GET)
-
-
-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" % (url, get_querydict.urlencode(safe='/')))
-    return redirect(view_name)
-
-
-# -----------------------------------------------------------------------------
-# OpenID Provider
-# -----------------------------------------------------------------------------
-
-
-def get_xrds_url(resource, request):
-    """
-    Return the XRDS url for a resource
-    """
-    host = request.get_host()
-
-    location = host + '/openid/provider/' + resource + '/'
-
-    if request.is_secure():
-        return 'https://' + location
-    else:
-        return 'http://' + location
-
-
-def add_openid_simple_registration(request, response, data):
-    sreg_data = {}
-    sreg_request = sreg.SRegRequest.fromOpenIDRequest(request)
-    sreg_fields = sreg_request.allRequestedFields()
-
-    # if consumer requested simple registration fields, add them
-    if sreg_fields:
-        for field in sreg_fields:
-            if field == 'email' and 'email' in data:
-                sreg_data['email'] = data['email']
-            elif field == 'fullname' and 'fullname' in data:
-                sreg_data['fullname'] = data['fullname']
-            elif field == 'nickname' and 'nickname' in data:
-                sreg_data['nickname'] = data['nickname']
-
-        # construct sreg response
-        sreg_response = sreg.SRegResponse.extractResponse(sreg_request,
-                                                          sreg_data)
-        sreg_response.toMessage(response.fields)
-
-
-def add_openid_attribute_exchange(request, response, data):
-    try:
-        ax_request = ax.FetchRequest.fromOpenIDRequest(request)
-    except ax.AXError:
-        #  not using OpenID attribute exchange extension
-        pass
-    else:
-        ax_response = ax.FetchResponse()
-
-        # if consumer requested attribute exchange fields, add them
-        if ax_request and ax_request.requested_attributes:
-            for type_uri in ax_request.requested_attributes.iterkeys():
-                email_schema = 'http://axschema.org/contact/email'
-                name_schema = 'http://axschema.org/namePerson'
-                if type_uri == email_schema and 'email' in data:
-                    ax_response.addValue(email_schema, data['email'])
-                elif type_uri == name_schema and 'fullname' in data:
-                    ax_response.addValue(name_schema, data['fullname'])
-
-            # construct ax response
-            ax_response.toMessage(response.fields)
-
-
-def provider_respond(server, request, response, data):
-    """
-    Respond to an OpenID request
-    """
-    # get and add extensions
-    add_openid_simple_registration(request, response, data)
-    add_openid_attribute_exchange(request, response, data)
-
-    # create http response from OpenID response
-    webresponse = server.encodeResponse(response)
-    http_response = HttpResponse(webresponse.body)
-    http_response.status_code = webresponse.code
-
-    # add OpenID headers to response
-    for k, v in webresponse.headers.iteritems():
-        http_response[k] = v
-
-    return http_response
-
-
-def validate_trust_root(openid_request):
-    """
-    Only allow OpenID requests from valid trust roots
-    """
-
-    trusted_roots = getattr(settings, 'OPENID_PROVIDER_TRUSTED_ROOT', None)
-
-    if not trusted_roots:
-        # not using trusted roots
-        return True
-
-    # don't allow empty trust roots
-    if (not hasattr(openid_request, 'trust_root') or
-            not openid_request.trust_root):
-        log.error('no trust_root')
-        return False
-
-    # ensure trust root parses cleanly (one wildcard, of form *.foo.com, etc.)
-    trust_root = TrustRoot.parse(openid_request.trust_root)
-    if not trust_root:
-        log.error('invalid trust_root')
-        return False
-
-    # don't allow empty return tos
-    if (not hasattr(openid_request, 'return_to') or
-            not openid_request.return_to):
-        log.error('empty return_to')
-        return False
-
-    # ensure return to is within trust root
-    if not trust_root.validateURL(openid_request.return_to):
-        log.error('invalid return_to')
-        return False
-
-    # check that the root matches the ones we trust
-    if not any(r for r in trusted_roots if fnmatch.fnmatch(trust_root, r)):
-        log.error('non-trusted root')
-        return False
-
-    return True
-
-
-@csrf_exempt
-def provider_login(request):
-    """
-    OpenID login endpoint
-    """
-
-    # make and validate endpoint
-    endpoint = get_xrds_url('login', request)
-    if not endpoint:
-        return default_render_failure(request, "Invalid OpenID request")
-
-    # initialize store and server
-    store = DjangoOpenIDStore()
-    server = Server(store, endpoint)
-
-    # first check to see if the request is an OpenID request.
-    # If so, the client will have specified an 'openid.mode' as part
-    # of the request.
-    if request.method == 'GET':
-        querydict = dict(request.GET.items())
-    else:
-        querydict = dict(request.POST.items())
-    error = False
-    if 'openid.mode' in request.GET or 'openid.mode' in request.POST:
-        # decode request
-        try:
-            openid_request = server.decodeRequest(querydict)
-        except (UntrustedReturnURL, ProtocolError):
-            openid_request = None
-
-        if not openid_request:
-            return default_render_failure(request, "Invalid OpenID request")
-
-        # don't allow invalid and non-trusted trust roots
-        if not validate_trust_root(openid_request):
-            return default_render_failure(request, "Invalid OpenID trust root")
-
-        # checkid_immediate not supported, require user interaction
-        if openid_request.mode == 'checkid_immediate':
-            return provider_respond(server, openid_request,
-                                    openid_request.answer(False), {})
-
-        # checkid_setup, so display login page
-        # (by falling through to the provider_login at the
-        # bottom of this method).
-        elif openid_request.mode == 'checkid_setup':
-            if openid_request.idSelect():
-                # remember request and original path
-                request.session['openid_setup'] = {
-                    'request': openid_request,
-                    'url': request.get_full_path(),
-                    'post_params': request.POST,
-                }
-
-                # user failed login on previous attempt
-                if 'openid_error' in request.session:
-                    error = True
-                    del request.session['openid_error']
-
-        # OpenID response
-        else:
-            return provider_respond(server, openid_request,
-                                    server.handleRequest(openid_request), {})
-
-    # handle login redirection:  these are also sent to this view function,
-    # but are distinguished by lacking the openid mode.  We also know that
-    # they are posts, because they come from the popup
-    elif request.method == 'POST' and 'openid_setup' in request.session:
-        # get OpenID request from session
-        openid_setup = request.session['openid_setup']
-        openid_request = openid_setup['request']
-        openid_request_url = openid_setup['url']
-        post_params = openid_setup['post_params']
-        # We need to preserve the parameters, and the easiest way to do this is
-        # through the URL
-        url_post_params = {
-            param: post_params[param] for param in post_params if param.startswith('openid')
-        }
-
-        encoded_params = urllib.urlencode(url_post_params)
-
-        if '?' not in openid_request_url:
-            openid_request_url = openid_request_url + '?' + encoded_params
-        else:
-            openid_request_url = openid_request_url + '&' + encoded_params
-
-        del request.session['openid_setup']
-
-        # don't allow invalid trust roots
-        if not validate_trust_root(openid_request):
-            return default_render_failure(request, "Invalid OpenID trust root")
-
-        # check if user with given email exists
-        # Failure is redirected to this method (by using the original URL),
-        # which will bring up the login dialog.
-        email = request.POST.get('email', None)
-        try:
-            user = User.objects.get(email=email)
-        except User.DoesNotExist:
-            request.session['openid_error'] = True
-            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-                AUDIT_LOG.warning(u"OpenID login failed - Unknown user email")
-            else:
-                msg = u"OpenID login failed - Unknown user email: {0}".format(email)
-                AUDIT_LOG.warning(msg)
-            return HttpResponseRedirect(openid_request_url)
-
-        # attempt to authenticate user (but not actually log them in...)
-        # Failure is again redirected to the login dialog.
-        username = user.username
-        password = request.POST.get('password', None)
-        try:
-            user = authenticate(username=username, password=password, request=request)
-        except RateLimitException:
-            AUDIT_LOG.warning(u'OpenID - Too many failed login attempts.')
-            return HttpResponseRedirect(openid_request_url)
-
-        if user is None:
-            request.session['openid_error'] = True
-            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-                AUDIT_LOG.warning(u"OpenID login failed - invalid password")
-            else:
-                AUDIT_LOG.warning(
-                    u"OpenID login failed - password for %s is invalid", email)
-            return HttpResponseRedirect(openid_request_url)
-
-        # authentication succeeded, so fetch user information
-        # that was requested
-        if user is not None and user.is_active:
-            # remove error from session since login succeeded
-            if 'openid_error' in request.session:
-                del request.session['openid_error']
-
-            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-                AUDIT_LOG.info(u"OpenID login success - user.id: %s", user.id)
-            else:
-                AUDIT_LOG.info(
-                    u"OpenID login success - %s (%s)", user.username, user.email)
-            # redirect user to return_to location
-            url = endpoint + urlquote(user.username)
-            response = openid_request.answer(True, None, url)
-
-            # Note too that this is hardcoded, and not really responding to
-            # the extensions that were registered in the first place.
-            results = {
-                'nickname': user.username,
-                'email': user.email,
-                'fullname': user.profile.name,
-            }
-
-            # the request succeeded:
-            return provider_respond(server, openid_request, response, results)
-
-        # the account is not active, so redirect back to the login page:
-        request.session['openid_error'] = True
-        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
-            AUDIT_LOG.warning(
-                u"Login failed - Account not active for user.id %s", user.id)
-        else:
-            AUDIT_LOG.warning(
-                u"Login failed - Account not active for user %s", username)
-        return HttpResponseRedirect(openid_request_url)
-
-    # determine consumer domain if applicable
-    return_to = request.GET.get('openid.return_to') or request.POST.get('openid.return_to') or ''
-    if return_to:
-        matches = re.match(r'\w+:\/\/([\w\.-]+)', return_to)
-        return_to = matches.group(1)
-
-    # display login page
-    response = render_to_response('provider_login.html', {
-        'error': error,
-        'return_to': return_to
-    })
-
-    # add custom XRDS header necessary for discovery process
-    response['X-XRDS-Location'] = get_xrds_url('xrds', request)
-    return response
-
-
-def provider_identity(request):
-    """
-    XRDS for identity discovery
-    """
-
-    response = render_to_response('identity.xml',
-                                  {'url': get_xrds_url('login', request)},
-                                  content_type='text/xml')
-
-    # custom XRDS header necessary for discovery process
-    response['X-XRDS-Location'] = get_xrds_url('identity', request)
-    return response
-
-
-def provider_xrds(request):
-    """
-    XRDS for endpoint discovery
-    """
-
-    response = render_to_response('xrds.xml',
-                                  {'url': get_xrds_url('login', request)},
-                                  content_type='text/xml')
-
-    # custom XRDS header necessary for discovery process
-    response['X-XRDS-Location'] = get_xrds_url('xrds', request)
-    return response
diff --git a/common/djangoapps/student/tests/test_create_account.py b/common/djangoapps/student/tests/test_create_account.py
index 7a2958d..656e9cd 100644
--- a/common/djangoapps/student/tests/test_create_account.py
+++ b/common/djangoapps/student/tests/test_create_account.py
@@ -16,7 +16,7 @@ import mock
 from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
 from lang_pref import LANGUAGE_KEY
 from notification_prefs import NOTIFICATION_PREF_KEY
-from external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
 import student
 from student.models import UserAttribute
 from student.views import REGISTRATION_AFFILIATE_ID
diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py
index 4ce14fd..3699289 100644
--- a/common/djangoapps/student/tests/test_login.py
+++ b/common/djangoapps/student/tests/test_login.py
@@ -16,7 +16,7 @@ import httpretty
 from mock import patch
 from social.apps.django_app.default.models import UserSocialAuth
 
-from external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
 from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
 from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory
 from student.views import login_oauth_token
diff --git a/common/djangoapps/student/tests/test_password_policy.py b/common/djangoapps/student/tests/test_password_policy.py
index 2c8fe1b..2eae69f 100644
--- a/common/djangoapps/student/tests/test_password_policy.py
+++ b/common/djangoapps/student/tests/test_password_policy.py
@@ -11,7 +11,7 @@ from importlib import import_module
 from django.test.utils import override_settings
 from django.conf import settings
 from mock import patch
-from external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
 from student.views import create_account
 
 
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index e63b527..198d55e 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -78,9 +78,9 @@ from courseware.access import has_access
 
 from django_comment_common.models import Role
 
-from external_auth.models import ExternalAuthMap
-import external_auth.views
-from external_auth.login_and_register import (
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
+import openedx.core.djangoapps.external_auth.views
+from openedx.core.djangoapps.external_auth.login_and_register import (
     login as external_auth_login,
     register as external_auth_register
 )
@@ -470,7 +470,9 @@ def register_user(request, extra_context=None):
     if extra_context is not None:
         context.update(extra_context)
 
-    if context.get("extauth_domain", '').startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX):
+    if context.get("extauth_domain", '').startswith(
+            openedx.core.djangoapps.external_auth.views.SHIBBOLETH_DOMAIN_PREFIX
+    ):
         return render_to_response('register-shib.html', context)
 
     # If third-party auth is enabled, prepopulate the form with data from the
@@ -1195,7 +1197,7 @@ def login_user(request, error=""):  # pylint: disable=too-many-statements,unused
     if settings.FEATURES.get('AUTH_USE_SHIB') and user:
         try:
             eamap = ExternalAuthMap.objects.get(user=user)
-            if eamap.external_domain.startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX):
+            if eamap.external_domain.startswith(openedx.core.djangoapps.external_auth.views.SHIBBOLETH_DOMAIN_PREFIX):
                 return JsonResponse({
                     "success": False,
                     "redirect": reverse('shib-login'),
@@ -1637,9 +1639,7 @@ def create_account_with_params(request, params):
         not settings.FEATURES.get("AUTH_USE_SHIB") or
         not settings.FEATURES.get("SHIB_DISABLE_TOS") or
         not do_external_auth or
-        not eamap.external_domain.startswith(
-            external_auth.views.SHIBBOLETH_DOMAIN_PREFIX
-        )
+        not eamap.external_domain.startswith(openedx.core.djangoapps.external_auth.views.SHIBBOLETH_DOMAIN_PREFIX)
     )
 
     form = AccountCreationForm(
diff --git a/docs/en_us/platform_api/source/conf.py b/docs/en_us/platform_api/source/conf.py
index 7f79a33..807b193 100644
--- a/docs/en_us/platform_api/source/conf.py
+++ b/docs/en_us/platform_api/source/conf.py
@@ -101,7 +101,7 @@ MOCK_MODULES = [
     'openid',
     'openid.store',
     'openid.store.interface',
-    'external_auth.views',
+    'openedx.core.djangoapps.external_auth.views',
     'mail_utils',
     'ratelimitbackend.backends',
     'social.apps.django_app.default',
diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py
index e6c21c5..c32b931 100644
--- a/lms/djangoapps/branding/views.py
+++ b/lms/djangoapps/branding/views.py
@@ -61,7 +61,7 @@ def index(request):
             return redirect(reverse('dashboard'))
 
     if settings.FEATURES.get('AUTH_USE_CERTIFICATES'):
-        from external_auth.views import ssl_login
+        from openedx.core.djangoapps.external_auth.views import ssl_login
         # Set next URL to dashboard if it isn't set to avoid
         # caching a redirect to / that causes a redirect loop on logout
         if not request.GET.get('next'):
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index d458403..43776a3 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -33,7 +33,7 @@ from xmodule.x_module import XModule
 from xmodule.split_test_module import get_split_user_partitions
 from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPartitionGroupError
 
-from external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
 from courseware.masquerade import get_masquerade_role, is_masquerading_as_student
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from student import auth
diff --git a/lms/djangoapps/dashboard/sysadmin.py b/lms/djangoapps/dashboard/sysadmin.py
index 5ef36f7..dd6e320 100644
--- a/lms/djangoapps/dashboard/sysadmin.py
+++ b/lms/djangoapps/dashboard/sysadmin.py
@@ -34,8 +34,8 @@ import dashboard.git_import as git_import
 from dashboard.git_import import GitImportError
 from student.roles import CourseStaffRole, CourseInstructorRole
 from dashboard.models import CourseImportLog
-from external_auth.models import ExternalAuthMap
-from external_auth.views import generate_password
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.views import generate_password
 from student.models import CourseEnrollment, UserProfile, Registration
 import track.views
 from xmodule.modulestore.django import modulestore
diff --git a/lms/djangoapps/lms_migration/management/commands/create_user.py b/lms/djangoapps/lms_migration/management/commands/create_user.py
index 03ebff0..4f70c49 100644
--- a/lms/djangoapps/lms_migration/management/commands/create_user.py
+++ b/lms/djangoapps/lms_migration/management/commands/create_user.py
@@ -15,7 +15,7 @@ import readline
 
 from django.core.management.base import BaseCommand
 from student.models import UserProfile, Registration
-from external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
 from django.contrib.auth.models import User, Group
 from pytz import UTC
 
diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py
index 645f573..05629ea 100644
--- a/lms/djangoapps/student_account/views.py
+++ b/lms/djangoapps/student_account/views.py
@@ -22,7 +22,7 @@ from edxmako.shortcuts import render_to_response
 import pytz
 
 from commerce.models import CommerceConfiguration
-from external_auth.login_and_register import (
+from openedx.core.djangoapps.external_auth.login_and_register import (
     login as external_auth_login,
     register as external_auth_register
 )
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 8d6c33d..a193d09 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1917,7 +1917,7 @@ INSTALLED_APPS = (
     'support',
 
     # External auth (OpenID, shib)
-    'external_auth',
+    'openedx.core.djangoapps.external_auth',
     'django_openid_auth',
 
     # django-oauth2-provider (deprecated)
diff --git a/lms/urls.py b/lms/urls.py
index 2e165ec..69ae6b4 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -803,27 +803,31 @@ if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
 if settings.FEATURES.get('AUTH_USE_OPENID'):
     urlpatterns += (
         url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'),
-        url(r'^openid/complete/$', 'external_auth.views.openid_login_complete', name='openid-complete'),
+        url(
+            r'^openid/complete/$',
+            'openedx.core.djangoapps.external_auth.views.openid_login_complete',
+            name='openid-complete',
+        ),
         url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'),
     )
 
 if settings.FEATURES.get('AUTH_USE_SHIB'):
     urlpatterns += (
-        url(r'^shib-login/$', 'external_auth.views.shib_login', name='shib-login'),
+        url(r'^shib-login/$', 'openedx.core.djangoapps.external_auth.views.shib_login', name='shib-login'),
     )
 
 if settings.FEATURES.get('AUTH_USE_CAS'):
     urlpatterns += (
-        url(r'^cas-auth/login/$', 'external_auth.views.cas_login', name="cas-login"),
+        url(r'^cas-auth/login/$', 'openedx.core.djangoapps.external_auth.views.cas_login', name="cas-login"),
         url(r'^cas-auth/logout/$', 'django_cas.views.logout', {'next_page': '/'}, name="cas-logout"),
     )
 
 if settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'):
     urlpatterns += (
         url(r'^course_specific_login/{}/$'.format(settings.COURSE_ID_PATTERN),
-            'external_auth.views.course_specific_login', name='course-specific-login'),
+            'openedx.core.djangoapps.external_auth.views.course_specific_login', name='course-specific-login'),
         url(r'^course_specific_register/{}/$'.format(settings.COURSE_ID_PATTERN),
-            'external_auth.views.course_specific_register', name='course-specific-register'),
+            'openedx.core.djangoapps.external_auth.views.course_specific_register', name='course-specific-register'),
 
     )
 
@@ -846,14 +850,26 @@ urlpatterns += (
 
 if settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
     urlpatterns += (
-        url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'),
+        url(
+            r'^openid/provider/login/$',
+            'openedx.core.djangoapps.external_auth.views.provider_login',
+            name='openid-provider-login',
+        ),
         url(
             r'^openid/provider/login/(?:.+)$',
-            'external_auth.views.provider_identity',
+            'openedx.core.djangoapps.external_auth.views.provider_identity',
             name='openid-provider-login-identity'
         ),
-        url(r'^openid/provider/identity/$', 'external_auth.views.provider_identity', name='openid-provider-identity'),
-        url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds')
+        url(
+            r'^openid/provider/identity/$',
+            'openedx.core.djangoapps.external_auth.views.provider_identity',
+            name='openid-provider-identity',
+        ),
+        url(
+            r'^openid/provider/xrds/$',
+            'openedx.core.djangoapps.external_auth.views.provider_xrds',
+            name='openid-provider-xrds',
+        ),
     )
 
 if settings.FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
diff --git a/openedx/core/djangoapps/external_auth/__init__.py b/openedx/core/djangoapps/external_auth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/__init__.py
diff --git a/openedx/core/djangoapps/external_auth/admin.py b/openedx/core/djangoapps/external_auth/admin.py
new file mode 100644
index 0000000..d6278e2
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/admin.py
@@ -0,0 +1,16 @@
+'''
+django admin pages for courseware model
+'''
+
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
+from ratelimitbackend import admin
+
+
+class ExternalAuthMapAdmin(admin.ModelAdmin):
+    """
+    Admin model for ExternalAuthMap
+    """
+    search_fields = ['external_id', 'user__username']
+    date_hierarchy = 'dtcreated'
+
+admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin)
diff --git a/openedx/core/djangoapps/external_auth/djangostore.py b/openedx/core/djangoapps/external_auth/djangostore.py
new file mode 100644
index 0000000..0e16f77
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/djangostore.py
@@ -0,0 +1,132 @@
+"""A openid store using django cache"""
+
+from openid.store.interface import OpenIDStore
+from openid.store import nonce
+
+from django.core.cache import cache
+
+import logging
+import time
+
+DEFAULT_ASSOCIATIONS_TIMEOUT = 60
+DEFAULT_NONCE_TIMEOUT = 600
+
+ASSOCIATIONS_KEY_PREFIX = 'openid.provider.associations.'
+NONCE_KEY_PREFIX = 'openid.provider.nonce.'
+
+log = logging.getLogger('DjangoOpenIDStore')
+
+
+def get_url_key(server_url):
+    """
+    Returns the URL key for the given server_url.
+    """
+    return ASSOCIATIONS_KEY_PREFIX + server_url
+
+
+def get_nonce_key(server_url, timestamp, salt):
+    """
+    Returns the nonce for the given parameters.
+    """
+    return '{prefix}{url}.{ts}.{salt}'.format(
+        prefix=NONCE_KEY_PREFIX,
+        url=server_url,
+        ts=timestamp,
+        salt=salt,
+    )
+
+
+class DjangoOpenIDStore(OpenIDStore):
+    """
+    django implementation of OpenIDStore.
+    """
+    def __init__(self):
+        log.info('DjangoStore cache:' + str(cache.__class__))
+
+    def storeAssociation(self, server_url, assoc):
+        key = get_url_key(server_url)
+
+        log.info('storeAssociation {0}'.format(key))
+
+        associations = cache.get(key, {})
+        associations[assoc.handle] = assoc
+
+        cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
+
+    def getAssociation(self, server_url, handle=None):
+        key = get_url_key(server_url)
+
+        log.info('getAssociation {0}'.format(key))
+
+        associations = cache.get(key, {})
+
+        assoc = None
+
+        if handle is None:
+            # get best association
+            valid_assocs = [a for a in associations if a.getExpiresIn() > 0]
+            if valid_assocs:
+                valid_assocs.sort(lambda a: a.getExpiresIn(), reverse=True)
+                assoc = valid_assocs.sort[0]
+        else:
+            assoc = associations.get(handle)
+
+        # check expiration and remove if it has expired
+        if assoc and assoc.getExpiresIn() <= 0:
+            if handle is None:
+                cache.delete(key)
+            else:
+                associations.pop(handle)
+                cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
+            assoc = None
+
+        return assoc
+
+    def removeAssociation(self, server_url, handle):
+        key = get_url_key(server_url)
+
+        log.info('removeAssociation {0}'.format(key))
+
+        associations = cache.get(key, {})
+
+        removed = False
+
+        if associations:
+            if handle is None:
+                cache.delete(key)
+                removed = True
+            else:
+                assoc = associations.pop(handle, None)
+                if assoc:
+                    cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
+                    removed = True
+
+        return removed
+
+    def useNonce(self, server_url, timestamp, salt):
+        key = get_nonce_key(server_url, timestamp, salt)
+
+        log.info('useNonce {0}'.format(key))
+
+        if abs(timestamp - time.time()) > nonce.SKEW:
+            return False
+
+        anonce = cache.get(key)
+
+        found = False
+
+        if anonce is None:
+            cache.set(key, '-', DEFAULT_NONCE_TIMEOUT)
+            found = False
+        else:
+            found = True
+
+        return found
+
+    def cleanupNonces(self):
+        # not necesary, keys will timeout
+        return 0
+
+    def cleanupAssociations(self):
+        # not necesary, keys will timeout
+        return 0
diff --git a/openedx/core/djangoapps/external_auth/login_and_register.py b/openedx/core/djangoapps/external_auth/login_and_register.py
new file mode 100644
index 0000000..20a4f80
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/login_and_register.py
@@ -0,0 +1,98 @@
+"""Intercept login and registration requests.
+
+This module contains legacy code originally from `student.views`.
+"""
+import re
+
+from django.conf import settings
+from django.shortcuts import redirect
+from django.core.urlresolvers import reverse
+import openedx.core.djangoapps.external_auth.views
+
+from xmodule.modulestore.django import modulestore
+from opaque_keys.edx.keys import CourseKey
+
+
+# pylint: disable=fixme
+# TODO: This function is kind of gnarly/hackish/etc and is only used in one location.
+# It'd be awesome if we could get rid of it; manually parsing course_id strings form larger strings
+# seems Probably Incorrect
+def _parse_course_id_from_string(input_str):
+    """
+    Helper function to determine if input_str (typically the queryparam 'next') contains a course_id.
+    @param input_str:
+    @return: the course_id if found, None if not
+    """
+    m_obj = re.match(r'^/courses/{}'.format(settings.COURSE_ID_PATTERN), input_str)
+    if m_obj:
+        return CourseKey.from_string(m_obj.group('course_id'))
+    return None
+
+
+def _get_course_enrollment_domain(course_id):
+    """
+    Helper function to get the enrollment domain set for a course with id course_id
+    @param course_id:
+    @return:
+    """
+    course = modulestore().get_course(course_id)
+    if course is None:
+        return None
+
+    return course.enrollment_domain
+
+
+def login(request):
+    """Allow external auth to intercept and handle a login request.
+
+    Arguments:
+        request (Request): A request for the login page.
+
+    Returns:
+        Response or None
+
+    """
+    # Default to a `None` response, indicating that external auth
+    # is not handling the request.
+    response = None
+
+    if (
+            settings.FEATURES['AUTH_USE_CERTIFICATES'] and
+            openedx.core.djangoapps.external_auth.views.ssl_get_cert_from_request(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.
+        response = openedx.core.djangoapps.external_auth.views.redirect_with_get('root', request.GET)
+    elif settings.FEATURES.get('AUTH_USE_CAS'):
+        # If CAS is enabled, redirect auth handling to there
+        response = redirect(reverse('cas-login'))
+    elif settings.FEATURES.get('AUTH_USE_SHIB'):
+        redirect_to = request.GET.get('next')
+        if redirect_to:
+            course_id = _parse_course_id_from_string(redirect_to)
+            if course_id and _get_course_enrollment_domain(course_id):
+                response = openedx.core.djangoapps.external_auth.views.course_specific_login(
+                    request,
+                    course_id.to_deprecated_string(),
+                )
+
+    return response
+
+
+def register(request):
+    """Allow external auth to intercept and handle a registration request.
+
+    Arguments:
+        request (Request): A request for the registration page.
+
+    Returns:
+        Response or None
+
+    """
+    response = 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.
+        response = openedx.core.djangoapps.external_auth.views.redirect_with_get('root', request.GET)
+    return response
diff --git a/openedx/core/djangoapps/external_auth/migrations/0001_initial.py b/openedx/core/djangoapps/external_auth/migrations/0001_initial.py
new file mode 100644
index 0000000..f68b8f0
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ExternalAuthMap',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('external_id', models.CharField(max_length=255, db_index=True)),
+                ('external_domain', models.CharField(max_length=255, db_index=True)),
+                ('external_credentials', models.TextField(blank=True)),
+                ('external_email', models.CharField(max_length=255, db_index=True)),
+                ('external_name', models.CharField(db_index=True, max_length=255, blank=True)),
+                ('internal_password', models.CharField(max_length=31, blank=True)),
+                ('dtcreated', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
+                ('dtsignup', models.DateTimeField(null=True, verbose_name=b'signup date')),
+                ('user', models.OneToOneField(null=True, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='externalauthmap',
+            unique_together=set([('external_id', 'external_domain')]),
+        ),
+    ]
diff --git a/openedx/core/djangoapps/external_auth/migrations/__init__.py b/openedx/core/djangoapps/external_auth/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/migrations/__init__.py
diff --git a/openedx/core/djangoapps/external_auth/models.py b/openedx/core/djangoapps/external_auth/models.py
new file mode 100644
index 0000000..03e076e
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/models.py
@@ -0,0 +1,35 @@
+"""
+WE'RE USING MIGRATIONS!
+
+If you make changes to this model, be sure to create an appropriate migration
+file and check it in at the same time as your model changes. To do that,
+
+1. Go to the edx-platform dir
+2. ./manage.py lms schemamigration student --auto description_of_your_change
+3. Add the migration file created in edx-platform/openedx/core/djangoapps/external_auth/migrations/
+"""
+
+from django.db import models
+from django.contrib.auth.models import User
+
+
+class ExternalAuthMap(models.Model):
+    """
+    Model class for external auth.
+    """
+    class Meta(object):
+        app_label = "external_auth"
+        unique_together = (('external_id', 'external_domain'), )
+
+    external_id = models.CharField(max_length=255, db_index=True)
+    external_domain = models.CharField(max_length=255, db_index=True)
+    external_credentials = models.TextField(blank=True)  # JSON dictionary
+    external_email = models.CharField(max_length=255, db_index=True)
+    external_name = models.CharField(blank=True, max_length=255, db_index=True)
+    user = models.OneToOneField(User, unique=True, db_index=True, null=True)
+    internal_password = models.CharField(blank=True, max_length=31)  	# randomly generated
+    dtcreated = models.DateTimeField('creation date', auto_now_add=True)
+    dtsignup = models.DateTimeField('signup date', null=True)		# set after signup
+
+    def __unicode__(self):
+        return "[%s] = (%s / %s)" % (self.external_id, self.external_name, self.external_email)
diff --git a/openedx/core/djangoapps/external_auth/tests/__init__.py b/openedx/core/djangoapps/external_auth/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/tests/__init__.py
diff --git a/openedx/core/djangoapps/external_auth/tests/test_helper.py b/openedx/core/djangoapps/external_auth/tests/test_helper.py
new file mode 100644
index 0000000..b028d62
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/tests/test_helper.py
@@ -0,0 +1,29 @@
+"""
+Tests for utility functions in external_auth module
+"""
+from django.test import TestCase
+from openedx.core.djangoapps.external_auth.views import _safe_postlogin_redirect
+
+
+class ExternalAuthHelperFnTest(TestCase):
+    """
+    Unit tests for the external_auth.views helper function
+    """
+    def test__safe_postlogin_redirect(self):
+        """
+        Tests the _safe_postlogin_redirect function with different values of next
+        """
+        HOST = 'testserver'                               # pylint: disable=invalid-name
+        ONSITE1 = '/dashboard'                            # pylint: disable=invalid-name
+        ONSITE2 = '/courses/org/num/name/courseware'      # pylint: disable=invalid-name
+        ONSITE3 = 'http://{}/my/custom/url'.format(HOST)  # pylint: disable=invalid-name
+        OFFSITE1 = 'http://www.attacker.com'              # pylint: disable=invalid-name
+
+        for redirect_to in [ONSITE1, ONSITE2, ONSITE3]:
+            redir = _safe_postlogin_redirect(redirect_to, HOST)
+            self.assertEqual(redir.status_code, 302)
+            self.assertEqual(redir['location'], redirect_to)
+
+        redir2 = _safe_postlogin_redirect(OFFSITE1, HOST)
+        self.assertEqual(redir2.status_code, 302)
+        self.assertEqual("/", redir2['location'])
diff --git a/openedx/core/djangoapps/external_auth/tests/test_openid_provider.py b/openedx/core/djangoapps/external_auth/tests/test_openid_provider.py
new file mode 100644
index 0000000..174715b
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/tests/test_openid_provider.py
@@ -0,0 +1,473 @@
+#-*- encoding=utf-8 -*-
+'''
+Created on Jan 18, 2013
+
+@author: brian
+'''
+import openid
+from openid.fetchers import HTTPFetcher, HTTPResponse
+from urlparse import parse_qs, urlparse
+
+from django.conf import settings
+from django.test import TestCase, LiveServerTestCase
+from django.core.cache import cache
+from django.test.utils import override_settings
+from django.core.urlresolvers import reverse
+from django.test.client import RequestFactory
+from unittest import skipUnless
+
+from student.tests.factories import UserFactory
+from openedx.core.djangoapps.external_auth.views import provider_login
+
+
+class MyFetcher(HTTPFetcher):
+    """A fetcher that uses server-internal calls for performing HTTP
+    requests.
+    """
+
+    def __init__(self, client):
+        """@param client: A test client object"""
+
+        super(MyFetcher, self).__init__()
+        self.client = client
+
+    def fetch(self, url, body=None, headers=None):
+        """Perform an HTTP request
+
+        @raises Exception: Any exception that can be raised by Django
+
+        @see: C{L{HTTPFetcher.fetch}}
+        """
+        if body:
+            # method = 'POST'
+            # undo the URL encoding of the POST arguments
+            data = parse_qs(body)
+            response = self.client.post(url, data)
+        else:
+            # method = 'GET'
+            data = {}
+            if headers and 'Accept' in headers:
+                data['CONTENT_TYPE'] = headers['Accept']
+            response = self.client.get(url, data)
+
+        # Translate the test client response to the fetcher's HTTP response abstraction
+        content = response.content
+        final_url = url
+        response_headers = {}
+        if 'Content-Type' in response:
+            response_headers['content-type'] = response['Content-Type']
+        if 'X-XRDS-Location' in response:
+            response_headers['x-xrds-location'] = response['X-XRDS-Location']
+        status = response.status_code
+
+        return HTTPResponse(
+            body=content,
+            final_url=final_url,
+            headers=response_headers,
+            status=status,
+        )
+
+
+class OpenIdProviderTest(TestCase):
+    """
+    Tests of the OpenId login
+    """
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_begin_login_with_xrds_url(self):
+
+        # the provider URL must be converted to an absolute URL in order to be
+        # used as an openid provider.
+        provider_url = reverse('openid-provider-xrds')
+        factory = RequestFactory()
+        request = factory.request()
+        abs_provider_url = request.build_absolute_uri(location=provider_url)
+
+        # In order for this absolute URL to work (i.e. to get xrds, then authentication)
+        # in the test environment, we either need a live server that works with the default
+        # fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
+        # Here we do the latter:
+        fetcher = MyFetcher(self.client)
+        openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
+
+        # now we can begin the login process by invoking a local openid client,
+        # with a pointer to the (also-local) openid provider:
+        with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
+
+            url = reverse('openid-login')
+            resp = self.client.post(url)
+            code = 200
+            self.assertEqual(resp.status_code, code,
+                             "got code {0} for url '{1}'. Expected code {2}"
+                             .format(resp.status_code, url, code))
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_begin_login_with_login_url(self):
+
+        # the provider URL must be converted to an absolute URL in order to be
+        # used as an openid provider.
+        provider_url = reverse('openid-provider-login')
+        factory = RequestFactory()
+        request = factory.request()
+        abs_provider_url = request.build_absolute_uri(location=provider_url)
+
+        # In order for this absolute URL to work (i.e. to get xrds, then authentication)
+        # in the test environment, we either need a live server that works with the default
+        # fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
+        # Here we do the latter:
+        fetcher = MyFetcher(self.client)
+        openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
+
+        # now we can begin the login process by invoking a local openid client,
+        # with a pointer to the (also-local) openid provider:
+        with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
+            url = reverse('openid-login')
+            resp = self.client.post(url)
+            code = 200
+            self.assertEqual(resp.status_code, code,
+                             "got code {0} for url '{1}'. Expected code {2}"
+                             .format(resp.status_code, url, code))
+            for expected_input in (
+                    '<input name="openid.ns" type="hidden" value="http://specs.openid.net/auth/2.0" />',
+
+                    '<input name="openid.ns.ax" type="hidden" value="http://openid.net/srv/ax/1.0" />',
+
+                    '<input name="openid.ax.type.fullname" type="hidden" value="http://axschema.org/namePerson" />',
+
+                    '<input type="submit" value="Continue" />',
+
+                    '<input name="openid.ax.type.email" type="hidden" value="http://axschema.org/contact/email" />',
+
+                    '<input name="openid.ax.type.lastname" '
+                    'type="hidden" value="http://axschema.org/namePerson/last" />',
+
+                    '<input name="openid.ax.type.firstname" '
+                    'type="hidden" value="http://axschema.org/namePerson/first" />',
+
+                    '<input name="openid.ax.required" type="hidden" '
+                    'value="email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname" />',
+
+                    '<input name="openid.ax.type.nickname" '
+                    'type="hidden" value="http://axschema.org/namePerson/friendly" />',
+
+                    '<input name="openid.ax.type.old_email" '
+                    'type="hidden" value="http://schema.openid.net/contact/email" />',
+
+                    '<input name="openid.ax.type.old_nickname" '
+                    'type="hidden" value="http://schema.openid.net/namePerson/friendly" />',
+
+                    '<input name="openid.ax.type.old_fullname" '
+                    'type="hidden" value="http://schema.openid.net/namePerson" />',
+
+                    '<input name="openid.identity" '
+                    'type="hidden" value="http://specs.openid.net/auth/2.0/identifier_select" />',
+
+                    '<input name="openid.claimed_id" '
+                    'type="hidden" value="http://specs.openid.net/auth/2.0/identifier_select" />',
+
+                    # should work on the test server as well
+                    '<input name="openid.realm" '
+                    'type="hidden" value="http://testserver/" />',
+            ):
+                self.assertContains(resp, expected_input, html=True)
+
+            # not included here are elements that will vary from run to run:
+            # <input name="openid.return_to" type="hidden"
+            #   value="http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H" />
+            # <input name="openid.assoc_handle" type="hidden" value="{HMAC-SHA1}{50ff8120}{rh87+Q==}" />
+
+    def attempt_login(self, expected_code, login_method='POST', **kwargs):
+        """ Attempt to log in through the open id provider login """
+        url = reverse('openid-provider-login')
+        args = {
+            "openid.mode": "checkid_setup",
+            "openid.return_to": "http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H",
+            "openid.assoc_handle": "{HMAC-SHA1}{50ff8120}{rh87+Q==}",
+            "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
+            "openid.ns": "http://specs.openid.net/auth/2.0",
+            "openid.realm": "http://testserver/",
+            "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
+            "openid.ns.ax": "http://openid.net/srv/ax/1.0",
+            "openid.ax.mode": "fetch_request",
+            "openid.ax.required": "email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname",
+            "openid.ax.type.fullname": "http://axschema.org/namePerson",
+            "openid.ax.type.lastname": "http://axschema.org/namePerson/last",
+            "openid.ax.type.firstname": "http://axschema.org/namePerson/first",
+            "openid.ax.type.nickname": "http://axschema.org/namePerson/friendly",
+            "openid.ax.type.email": "http://axschema.org/contact/email",
+            "openid.ax.type.old_email": "http://schema.openid.net/contact/email",
+            "openid.ax.type.old_nickname": "http://schema.openid.net/namePerson/friendly",
+            "openid.ax.type.old_fullname": "http://schema.openid.net/namePerson",
+        }
+        # override the default args with any given arguments
+        for key in kwargs:
+            args["openid." + key] = kwargs[key]
+
+        if login_method == 'POST':
+            resp = self.client.post(url, args)
+        elif login_method == 'GET':
+            resp = self.client.get(url, args)
+        else:
+            self.fail('Invalid login method')
+
+        code = expected_code
+        self.assertEqual(resp.status_code, code,
+                         "got code {0} for url '{1}'. Expected code {2}"
+                         .format(resp.status_code, url, code))
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_open_id_setup(self):
+        """ Attempt a standard successful login """
+        self.attempt_login(200)
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_invalid_namespace(self):
+        """ Test for 403 error code when the namespace of the request is invalid"""
+        self.attempt_login(403, ns="http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0")
+
+    @override_settings(OPENID_PROVIDER_TRUSTED_ROOTS=['http://apps.cs50.edx.org'])
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_invalid_return_url(self):
+        """ Test for 403 error code when the url"""
+        self.attempt_login(403, return_to="http://apps.cs50.edx.or")
+
+    def _send_bad_redirection_login(self):
+        """
+        Attempt to log in to the provider with setup parameters
+
+        Intentionally fail the login to force a redirect
+        """
+        user = UserFactory()
+
+        factory = RequestFactory()
+        post_params = {'email': user.email, 'password': 'password'}
+        fake_url = 'fake url'
+        request = factory.post(reverse('openid-provider-login'), post_params)
+        openid_setup = {
+            'request': factory.request(),
+            'url': fake_url,
+            'post_params': {}
+        }
+        request.session = {
+            'openid_setup': openid_setup
+        }
+        response = provider_login(request)
+        return response
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_login_openid_handle_redirection(self):
+        """ Test to see that we can handle login redirection properly"""
+        response = self._send_bad_redirection_login()
+        self.assertEquals(response.status_code, 302)
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_login_openid_handle_redirection_ratelimited(self):
+        # try logging in 30 times, the default limit in the number of failed
+        # log in attempts before the rate gets limited
+        for _ in xrange(30):
+            self._send_bad_redirection_login()
+
+        response = self._send_bad_redirection_login()
+        # verify that we are not returning the default 403
+        self.assertEquals(response.status_code, 302)
+        # clear the ratelimit cache so that we don't fail other logins
+        cache.clear()
+
+    def _attempt_login_and_perform_final_response(self, user, profile_name):
+        """
+        Performs full procedure of a successful OpenID provider login for user,
+        all required data is taken form ``user`` attribute which is an instance
+        of ``User`` model. As a convenience this method will also set
+        ``profile.name`` for the user.
+        """
+        url = reverse('openid-provider-login')
+
+        # login to the client so that we can persist session information
+        user.profile.name = profile_name
+        user.profile.save()
+        # It is asssumed that user's password is test (default for UserFactory)
+        self.client.login(username=user.username, password='test')
+        # login once to get the right session information
+        self.attempt_login(200)
+        post_args = {
+            'email': user.email,
+            'password': 'test'
+        }
+
+        # call url again, this time with username and password
+        return self.client.post(url, post_args)
+
+    @skipUnless(
+        settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'), 'OpenID not enabled')
+    def test_provider_login_can_handle_unicode_email(self):
+        user = UserFactory(email=u"user.ąęł@gmail.com")
+        resp = self._attempt_login_and_perform_final_response(user, u"Jan ĄĘŁ")
+        location = resp['Location']
+        parsed_url = urlparse(location)
+        parsed_qs = parse_qs(parsed_url.query)
+        self.assertEquals(parsed_qs['openid.ax.type.ext1'][0], 'http://axschema.org/contact/email')
+        self.assertEquals(parsed_qs['openid.ax.type.ext0'][0], 'http://axschema.org/namePerson')
+        self.assertEquals(parsed_qs['openid.ax.value.ext0.1'][0],
+                          user.profile.name.encode('utf-8'))  # pylint: disable=no-member
+        self.assertEquals(parsed_qs['openid.ax.value.ext1.1'][0],
+                          user.email.encode('utf-8'))  # pylint: disable=no-member
+
+    @skipUnless(
+        settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'), 'OpenID not enabled')
+    def test_provider_login_can_handle_unicode_email_invalid_password(self):
+        user = UserFactory(email=u"user.ąęł@gmail.com")
+        url = reverse('openid-provider-login')
+
+        # login to the client so that we can persist session information
+        user.profile.name = u"Jan ĄĘ"
+        user.profile.save()
+        # It is asssumed that user's password is test (default for UserFactory)
+        self.client.login(username=user.username, password='test')
+        # login once to get the right session information
+        self.attempt_login(200)
+        # We trigger situation where user password is invalid at last phase
+        # of openid login
+        post_args = {
+            'email': user.email,
+            'password': 'invalid-password'
+        }
+
+        # call url again, this time with username and password
+        return self.client.post(url, post_args)
+
+    @skipUnless(
+        settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'), 'OpenID not enabled')
+    def test_provider_login_can_handle_unicode_email_inactive_account(self):
+        user = UserFactory(email=u"user.ąęł@gmail.com", username=u"ąęół")
+        url = reverse('openid-provider-login')
+
+        # login to the client so that we can persist session information
+        user.profile.name = u'Jan ĄĘ'
+        user.profile.save()  # pylint: disable=no-member
+        self.client.login(username=user.username, password='test')
+        # login once to get the right session information
+        self.attempt_login(200)
+        # We trigger situation where user is not active at final phase of
+        # OpenId login.
+        user.is_active = False
+        user.save()  # pylint: disable=no-member
+        post_args = {
+            'email': user.email,
+            'password': 'test'
+        }
+        # call url again, this time with username and password
+        self.client.post(url, post_args)
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_openid_final_response(self):
+
+        user = UserFactory()
+
+        # login to the client so that we can persist session information
+        for name in ['Robot 33', '☃']:
+            resp = self._attempt_login_and_perform_final_response(user, name)
+            # all information is embedded in the redirect url
+            location = resp['Location']
+            # parse the url
+            parsed_url = urlparse(location)
+            parsed_qs = parse_qs(parsed_url.query)
+            self.assertEquals(parsed_qs['openid.ax.type.ext1'][0], 'http://axschema.org/contact/email')
+            self.assertEquals(parsed_qs['openid.ax.type.ext0'][0], 'http://axschema.org/namePerson')
+            self.assertEquals(parsed_qs['openid.ax.value.ext1.1'][0], user.email)
+            self.assertEquals(parsed_qs['openid.ax.value.ext0.1'][0], user.profile.name)
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_openid_invalid_password(self):
+
+        url = reverse('openid-provider-login')
+        user = UserFactory()
+
+        # login to the client so that we can persist session information
+        for method in ['POST', 'GET']:
+            self.client.login(username=user.username, password='test')
+            self.attempt_login(200, method)
+            openid_setup = self.client.session['openid_setup']
+            self.assertIn('post_params', openid_setup)
+            post_args = {
+                'email': user.email,
+                'password': 'bad_password',
+            }
+
+            # call url again, this time with username and password
+            resp = self.client.post(url, post_args)
+            self.assertEquals(resp.status_code, 302)
+            redirect_url = resp['Location']
+            parsed_url = urlparse(redirect_url)
+            query_params = parse_qs(parsed_url[4])
+            self.assertIn('openid.return_to', query_params)
+            self.assertTrue(
+                query_params['openid.return_to'][0].startswith('http://testserver/openid/complete/')
+            )
+
+
+class OpenIdProviderLiveServerTest(LiveServerTestCase):
+    """
+    In order for this absolute URL to work (i.e. to get xrds, then authentication)
+    in the test environment, we either need a live server that works with the default
+    fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
+    Here we do the former.
+    """
+
+    @skipUnless(settings.FEATURES.get('AUTH_USE_OPENID') and
+                settings.FEATURES.get('AUTH_USE_OPENID_PROVIDER'),
+                'OpenID not enabled')
+    def test_begin_login(self):
+        # the provider URL must be converted to an absolute URL in order to be
+        # used as an openid provider.
+        provider_url = reverse('openid-provider-xrds')
+        factory = RequestFactory()
+        request = factory.request()
+        abs_provider_url = request.build_absolute_uri(location=provider_url)
+
+        # In order for this absolute URL to work (i.e. to get xrds, then authentication)
+        # in the test environment, we either need a live server that works with the default
+        # fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
+        # Here we do the latter:
+        fetcher = MyFetcher(self.client)
+        openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
+
+        # now we can begin the login process by invoking a local openid client,
+        # with a pointer to the (also-local) openid provider:
+        with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
+            url = reverse('openid-login')
+            resp = self.client.post(url)
+            code = 200
+            self.assertEqual(resp.status_code, code,
+                             "got code {0} for url '{1}'. Expected code {2}"
+                             .format(resp.status_code, url, code))
+
+    @classmethod
+    def tearDownClass(cls):
+        """
+        Workaround for a runtime error that occurs
+        intermittently when the server thread doesn't shut down
+        within 2 seconds.
+
+        Since the server is running in a Django thread and will
+        be terminated when the test suite terminates,
+        this shouldn't cause a resource allocation issue.
+        """
+        try:
+            super(OpenIdProviderLiveServerTest, cls).tearDownClass()
+        except RuntimeError:
+            print "Warning: Could not shut down test server."
diff --git a/openedx/core/djangoapps/external_auth/tests/test_shib.py b/openedx/core/djangoapps/external_auth/tests/test_shib.py
new file mode 100644
index 0000000..59510ff
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/tests/test_shib.py
@@ -0,0 +1,597 @@
+# -*- coding: utf-8 -*-
+#pylint: disable=no-member
+"""
+Tests for Shibboleth Authentication
+@jbau
+"""
+import unittest
+
+from ddt import ddt, data
+from django.conf import settings
+from django.http import HttpResponseRedirect
+from django.test import TestCase
+from django.test.client import RequestFactory, Client as DjangoTestClient
+from django.test.utils import override_settings
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import AnonymousUser, User
+from importlib import import_module
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.views import (
+    shib_login, course_specific_login, course_specific_register, _flatten_to_ascii
+)
+from mock import patch
+from nose.plugins.attrib import attr
+from urllib import urlencode
+
+from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
+from student.views import change_enrollment
+from student.models import UserProfile, CourseEnrollment
+from student.tests.factories import UserFactory
+from xmodule.modulestore.tests.factories import CourseFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore import ModuleStoreEnum
+
+
+# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
+# attributes via request.META.  We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
+# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
+
+# For the sake of python convention we'll make all of these variable names ALL_CAPS
+# These values would all returned from request.META, so they need to be str, not unicode
+IDP = 'https://idp.stanford.edu/'
+REMOTE_USER = 'test_user@stanford.edu'
+MAILS = [None, '', 'test_user@stanford.edu']  # unicode shouldn't be in emails, would fail django's email validator
+DISPLAYNAMES = [None, '', 'Jason 包']
+GIVENNAMES = [None, '', 'jasön; John; bob']  # At Stanford, the givenNames can be a list delimited by ';'
+SNS = [None, '', '包; smith']  # At Stanford, the sns can be a list delimited by ';'
+
+
+def gen_all_identities():
+    """
+    A generator for all combinations of test inputs.
+    Each generated item is a dict that represents what a shib IDP
+    could potentially pass to django via request.META, i.e.
+    setting (or not) request.META['givenName'], etc.
+    """
+    def _build_identity_dict(mail, display_name, given_name, surname):
+        """ Helper function to return a dict of test identity """
+        meta_dict = {'Shib-Identity-Provider': IDP,
+                     'REMOTE_USER': REMOTE_USER}
+        if display_name is not None:
+            meta_dict['displayName'] = display_name
+        if mail is not None:
+            meta_dict['mail'] = mail
+        if given_name is not None:
+            meta_dict['givenName'] = given_name
+        if surname is not None:
+            meta_dict['sn'] = surname
+        return meta_dict
+
+    for mail in MAILS:
+        for given_name in GIVENNAMES:
+            for surname in SNS:
+                for display_name in DISPLAYNAMES:
+                    yield _build_identity_dict(mail, display_name, given_name, surname)
+
+
+@attr(shard=3)
+@ddt
+@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache')
+class ShibSPTest(CacheIsolationTestCase):
+    """
+    Tests for the Shibboleth SP, which communicates via request.META
+    (Apache environment variables set by mod_shib)
+    """
+
+    ENABLED_CACHES = ['default']
+
+    request_factory = RequestFactory()
+
+    def setUp(self):
+        super(ShibSPTest, self).setUp()
+        self.test_user_id = ModuleStoreEnum.UserID.test
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    def test_exception_shib_login(self):
+        """
+        Tests that we get the error page when there is no REMOTE_USER
+        or Shib-Identity-Provider in request.META
+        """
+        no_remote_user_response = self.client.get(reverse('shib-login'), HTTP_SHIB_IDENTITY_PROVIDER=IDP)
+        self.assertEqual(no_remote_user_response.status_code, 403)
+        self.assertIn("identity server did not return your ID information", no_remote_user_response.content)
+
+        no_idp_response = self.client.get(reverse('shib-login'), HTTP_REMOTE_USER=REMOTE_USER)
+        self.assertEqual(no_idp_response.status_code, 403)
+        self.assertIn("identity server did not return your ID information", no_idp_response.content)
+
+    def _assert_shib_login_is_logged(self, audit_log_call, remote_user):
+        """Asserts that shibboleth login attempt is being logged"""
+        remote_user = _flatten_to_ascii(remote_user)  # django usernames have to be ascii
+        method_name, args, _kwargs = audit_log_call
+        self.assertEquals(method_name, 'info')
+        self.assertEquals(len(args), 1)
+        self.assertIn(u'logged in via Shibboleth', args[0])
+        self.assertIn(remote_user, args[0])
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    def test_shib_login(self):
+        """
+        Tests that:
+          * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in
+          * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page
+          * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email
+            of an existing user without an existing ExternalAuthMap links the two and log the user in
+          * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email
+            of an existing user that already has an ExternalAuthMap causes an error (403)
+          * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear
+        """
+        # pylint: disable=too-many-statements
+
+        user_w_map = UserFactory.create(email='withmap@stanford.edu')
+        extauth = ExternalAuthMap(external_id='withmap@stanford.edu',
+                                  external_email='',
+                                  external_domain='shib:https://idp.stanford.edu/',
+                                  external_credentials="",
+                                  user=user_w_map)
+        user_wo_map = UserFactory.create(email='womap@stanford.edu')
+        user_w_map.save()
+        user_wo_map.save()
+        extauth.save()
+
+        inactive_user = UserFactory.create(email='inactive@stanford.edu')
+        inactive_user.is_active = False
+        inactive_extauth = ExternalAuthMap(external_id='inactive@stanford.edu',
+                                           external_email='',
+                                           external_domain='shib:https://idp.stanford.edu/',
+                                           external_credentials="",
+                                           user=inactive_user)
+        inactive_user.save()
+        inactive_extauth.save()
+
+        idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/']
+        remote_users = ['withmap@stanford.edu', 'womap@stanford.edu',
+                        'testuser2@someother_idp.com', 'inactive@stanford.edu']
+
+        for idp in idps:
+            for remote_user in remote_users:
+
+                self.client.logout()
+                with patch('openedx.core.djangoapps.external_auth.views.AUDIT_LOG') as mock_audit_log:
+                    response = self.client.get(
+                        reverse('shib-login'),
+                        **{
+                            'Shib-Identity-Provider': idp,
+                            'mail': remote_user,
+                            'REMOTE_USER': remote_user,
+                        }
+                    )
+                audit_log_calls = mock_audit_log.method_calls
+
+                if idp == "https://idp.stanford.edu/" and remote_user == 'withmap@stanford.edu':
+                    self.assertRedirects(response, '/dashboard')
+                    self.assertEquals(int(self.client.session['_auth_user_id']), user_w_map.id)
+                    # verify logging:
+                    self.assertEquals(len(audit_log_calls), 2)
+                    self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
+                    method_name, args, _kwargs = audit_log_calls[1]
+                    self.assertEquals(method_name, 'info')
+                    self.assertEquals(len(args), 1)
+                    self.assertIn(u'Login success', args[0])
+                    self.assertIn(remote_user, args[0])
+                elif idp == "https://idp.stanford.edu/" and remote_user == 'inactive@stanford.edu':
+                    self.assertEqual(response.status_code, 403)
+                    self.assertIn("Account not yet activated: please look for link in your email", response.content)
+                    # verify logging:
+                    self.assertEquals(len(audit_log_calls), 2)
+                    self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
+                    method_name, args, _kwargs = audit_log_calls[1]
+                    self.assertEquals(method_name, 'warning')
+                    self.assertEquals(len(args), 1)
+                    self.assertIn(u'is not active after external login', args[0])
+                    # self.assertEquals(remote_user, args[1])
+                elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu':
+                    self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map))
+                    self.assertRedirects(response, '/dashboard')
+                    self.assertEquals(int(self.client.session['_auth_user_id']), user_wo_map.id)
+                    # verify logging:
+                    self.assertEquals(len(audit_log_calls), 2)
+                    self._assert_shib_login_is_logged(audit_log_calls[0], remote_user)
+                    method_name, args, _kwargs = audit_log_calls[1]
+                    self.assertEquals(method_name, 'info')
+                    self.assertEquals(len(args), 1)
+                    self.assertIn(u'Login success', args[0])
+                    self.assertIn(remote_user, args[0])
+                elif idp == "https://someother.idp.com/" and remote_user in \
+                            ['withmap@stanford.edu', 'womap@stanford.edu', 'inactive@stanford.edu']:
+                    self.assertEqual(response.status_code, 403)
+                    self.assertIn("You have already created an account using an external login", response.content)
+                    # no audit logging calls
+                    self.assertEquals(len(audit_log_calls), 0)
+                else:
+                    self.assertEqual(response.status_code, 200)
+                    self.assertContains(response,
+                                        (u"Preferences for {platform_name}"
+                                         .format(platform_name=settings.PLATFORM_NAME)))
+                    # no audit logging calls
+                    self.assertEquals(len(audit_log_calls), 0)
+
+    def _test_auto_activate_user_with_flag(self, log_user_string="inactive@stanford.edu"):
+        """
+        Tests that FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] means extauth automatically
+        linked users, activates them, and logs them in
+        """
+        inactive_user = UserFactory.create(email='inactive@stanford.edu')
+        inactive_user.is_active = False
+        inactive_user.save()
+        request = self.request_factory.get('/shib-login')
+        request.session = import_module(settings.SESSION_ENGINE).SessionStore()  # empty session
+        request.META.update({
+            'Shib-Identity-Provider': 'https://idp.stanford.edu/',
+            'REMOTE_USER': 'inactive@stanford.edu',
+            'mail': 'inactive@stanford.edu'
+        })
+
+        request.user = AnonymousUser()
+        with patch('openedx.core.djangoapps.external_auth.views.AUDIT_LOG') as mock_audit_log:
+            response = shib_login(request)
+        audit_log_calls = mock_audit_log.method_calls
+        # reload user from db, since the view function works via db side-effects
+        inactive_user = User.objects.get(id=inactive_user.id)
+        self.assertIsNotNone(ExternalAuthMap.objects.get(user=inactive_user))
+        self.assertTrue(inactive_user.is_active)
+        self.assertIsInstance(response, HttpResponseRedirect)
+        self.assertEqual(request.user, inactive_user)
+        self.assertEqual(response['Location'], '/dashboard')
+        # verify logging:
+        self.assertEquals(len(audit_log_calls), 3)
+        self._assert_shib_login_is_logged(audit_log_calls[0], log_user_string)
+        method_name, args, _kwargs = audit_log_calls[2]
+        self.assertEquals(method_name, 'info')
+        self.assertEquals(len(args), 1)
+        self.assertIn(u'Login success', args[0])
+        self.assertIn(log_user_string, args[0])
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    @patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'SQUELCH_PII_IN_LOGS': False})
+    def test_extauth_auto_activate_user_with_flag_no_squelch(self):
+        """
+        Wrapper to run base_test_extauth_auto_activate_user_with_flag with {'SQUELCH_PII_IN_LOGS': False}
+        """
+        self._test_auto_activate_user_with_flag(log_user_string="inactive@stanford.edu")
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    @patch.dict(settings.FEATURES, {'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH': True, 'SQUELCH_PII_IN_LOGS': True})
+    def test_extauth_auto_activate_user_with_flag_squelch(self):
+        """
+        Wrapper to run base_test_extauth_auto_activate_user_with_flag with {'SQUELCH_PII_IN_LOGS': True}
+        """
+        self._test_auto_activate_user_with_flag(log_user_string="user.id: 1")
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    @data(*gen_all_identities())
+    def test_registration_form(self, identity):
+        """
+        Tests the registration form showing up with the proper parameters.
+
+        Uses django test client for its session support
+        """
+        client = DjangoTestClient()
+        # identity k/v pairs will show up in request.META
+        response = client.get(path='/shib-login/', data={}, follow=False, **identity)
+
+        self.assertEquals(response.status_code, 200)
+        mail_input_html = '<input class="" id="email" type="email" name="email"'
+        if not identity.get('mail'):
+            self.assertContains(response, mail_input_html)
+        else:
+            self.assertNotContains(response, mail_input_html)
+        sn_empty = not identity.get('sn')
+        given_name_empty = not identity.get('givenName')
+        displayname_empty = not identity.get('displayName')
+        fullname_input_html = '<input id="name" type="text" name="name"'
+        if sn_empty and given_name_empty and displayname_empty:
+            self.assertContains(response, fullname_input_html)
+        else:
+            self.assertNotContains(response, fullname_input_html)
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    @data(*gen_all_identities())
+    def test_registration_form_submit(self, identity):
+        """
+        Tests user creation after the registration form that pops is submitted.  If there is no shib
+        ExternalAuthMap in the session, then the created user should take the username and email from the
+        request.
+
+        Uses django test client for its session support
+        """
+        # First we pop the registration form
+        self.client.get(path='/shib-login/', data={}, follow=False, **identity)
+        # Then we have the user answer the registration form
+        # These are unicode because request.POST returns unicode
+        postvars = {'email': u'post_email@stanford.edu',
+                    'username': u'post_username',  # django usernames can't be unicode
+                    'password': u'post_pássword',
+                    'name': u'post_náme',
+                    'terms_of_service': u'true',
+                    'honor_code': u'true'}
+
+        with patch('student.views.AUDIT_LOG') as mock_audit_log:
+            self.client.post('/create_account', data=postvars)
+
+        mail = identity.get('mail')
+
+        # verify logging of login happening during account creation:
+        audit_log_calls = mock_audit_log.method_calls
+        self.assertEquals(len(audit_log_calls), 3)
+        method_name, args, _kwargs = audit_log_calls[0]
+        self.assertEquals(method_name, 'info')
+        self.assertEquals(len(args), 1)
+        self.assertIn(u'Login success on new account creation', args[0])
+        self.assertIn(u'post_username', args[0])
+        method_name, args, _kwargs = audit_log_calls[1]
+        self.assertEquals(method_name, 'info')
+        self.assertEquals(len(args), 2)
+        self.assertIn(u'User registered with external_auth', args[0])
+        self.assertEquals(u'post_username', args[1])
+        method_name, args, _kwargs = audit_log_calls[2]
+        self.assertEquals(method_name, 'info')
+        self.assertEquals(len(args), 3)
+        self.assertIn(u'Updated ExternalAuthMap for ', args[0])
+        self.assertEquals(u'post_username', args[1])
+        self.assertEquals(u'test_user@stanford.edu', args[2].external_id)
+
+        user = User.objects.get(id=self.client.session['_auth_user_id'])
+
+        # check that the created user has the right email, either taken from shib or user input
+        if mail:
+            self.assertEqual(user.email, mail)
+            self.assertEqual(list(User.objects.filter(email=postvars['email'])), [])
+            self.assertIsNotNone(User.objects.get(email=mail))  # get enforces only 1 such user
+        else:
+            self.assertEqual(user.email, postvars['email'])
+            self.assertEqual(list(User.objects.filter(email=mail)), [])
+            self.assertIsNotNone(User.objects.get(email=postvars['email']))  # get enforces only 1 such user
+
+        # check that the created user profile has the right name, either taken from shib or user input
+        profile = UserProfile.objects.get(user=user)
+        sn_empty = not identity.get('sn')
+        given_name_empty = not identity.get('givenName')
+        displayname_empty = not identity.get('displayName')
+
+        if displayname_empty:
+            if sn_empty and given_name_empty:
+                self.assertEqual(profile.name, postvars['name'])
+            else:
+                self.assertEqual(profile.name, self.client.session['ExternalAuthMap'].external_name)
+                self.assertNotIn(u';', profile.name)
+        else:
+            self.assertEqual(profile.name, self.client.session['ExternalAuthMap'].external_name)
+            self.assertEqual(profile.name, identity.get('displayName').decode('utf-8'))
+
+
+@ddt
+@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache')
+class ShibSPTestModifiedCourseware(ModuleStoreTestCase):
+    """
+    Tests for the Shibboleth SP which modify the courseware
+    """
+
+    ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
+
+    request_factory = RequestFactory()
+
+    def setUp(self):
+        super(ShibSPTestModifiedCourseware, self).setUp()
+        self.test_user_id = ModuleStoreEnum.UserID.test
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    @data(None, "", "shib:https://idp.stanford.edu/")
+    def test_course_specific_login_and_reg(self, domain):
+        """
+        Tests that the correct course specific login and registration urls work for shib
+        """
+        course = CourseFactory.create(
+            org='MITx',
+            number='999',
+            display_name='Robot Super Course',
+            user_id=self.test_user_id,
+        )
+
+        # Test for cases where course is found
+        # set domains
+
+        # temporarily set the branch to draft-preferred so we can update the course
+        with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
+            course.enrollment_domain = domain
+            self.store.update_item(course, self.test_user_id)
+
+        # setting location to test that GET params get passed through
+        login_request = self.request_factory.get('/course_specific_login/MITx/999/Robot_Super_Course' +
+                                                 '?course_id=MITx/999/Robot_Super_Course' +
+                                                 '&enrollment_action=enroll')
+        _reg_request = self.request_factory.get('/course_specific_register/MITx/999/Robot_Super_Course' +
+                                                '?course_id=MITx/999/course/Robot_Super_Course' +
+                                                '&enrollment_action=enroll')
+
+        login_response = course_specific_login(login_request, 'MITx/999/Robot_Super_Course')
+        reg_response = course_specific_register(login_request, 'MITx/999/Robot_Super_Course')
+
+        if domain and "shib" in domain:
+            self.assertIsInstance(login_response, HttpResponseRedirect)
+            self.assertEqual(login_response['Location'],
+                             reverse('shib-login') +
+                             '?course_id=MITx/999/Robot_Super_Course' +
+                             '&enrollment_action=enroll')
+            self.assertIsInstance(login_response, HttpResponseRedirect)
+            self.assertEqual(reg_response['Location'],
+                             reverse('shib-login') +
+                             '?course_id=MITx/999/Robot_Super_Course' +
+                             '&enrollment_action=enroll')
+        else:
+            self.assertIsInstance(login_response, HttpResponseRedirect)
+            self.assertEqual(login_response['Location'],
+                             reverse('signin_user') +
+                             '?course_id=MITx/999/Robot_Super_Course' +
+                             '&enrollment_action=enroll')
+            self.assertIsInstance(login_response, HttpResponseRedirect)
+            self.assertEqual(reg_response['Location'],
+                             reverse('register_user') +
+                             '?course_id=MITx/999/Robot_Super_Course' +
+                             '&enrollment_action=enroll')
+
+        # Now test for non-existent course
+        # setting location to test that GET params get passed through
+        login_request = self.request_factory.get('/course_specific_login/DNE/DNE/DNE' +
+                                                 '?course_id=DNE/DNE/DNE' +
+                                                 '&enrollment_action=enroll')
+        _reg_request = self.request_factory.get('/course_specific_register/DNE/DNE/DNE' +
+                                                '?course_id=DNE/DNE/DNE/Robot_Super_Course' +
+                                                '&enrollment_action=enroll')
+
+        login_response = course_specific_login(login_request, 'DNE/DNE/DNE')
+        reg_response = course_specific_register(login_request, 'DNE/DNE/DNE')
+
+        self.assertIsInstance(login_response, HttpResponseRedirect)
+        self.assertEqual(login_response['Location'],
+                         reverse('signin_user') +
+                         '?course_id=DNE/DNE/DNE' +
+                         '&enrollment_action=enroll')
+        self.assertIsInstance(login_response, HttpResponseRedirect)
+        self.assertEqual(reg_response['Location'],
+                         reverse('register_user') +
+                         '?course_id=DNE/DNE/DNE' +
+                         '&enrollment_action=enroll')
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    def test_enrollment_limit_by_domain(self):
+        """
+            Tests that the enrollmentDomain setting is properly limiting enrollment to those who have
+            the proper external auth
+        """
+
+        # create 2 course, one with limited enrollment one without
+        shib_course = CourseFactory.create(
+            org='Stanford',
+            number='123',
+            display_name='Shib Only',
+            enrollment_domain='shib:https://idp.stanford.edu/',
+            user_id=self.test_user_id,
+        )
+
+        open_enroll_course = CourseFactory.create(
+            org='MITx',
+            number='999',
+            display_name='Robot Super Course',
+            enrollment_domain='',
+            user_id=self.test_user_id,
+        )
+
+        # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth
+        shib_student = UserFactory.create()
+        shib_student.save()
+        extauth = ExternalAuthMap(external_id='testuser@stanford.edu',
+                                  external_email='',
+                                  external_domain='shib:https://idp.stanford.edu/',
+                                  external_credentials="",
+                                  user=shib_student)
+        extauth.save()
+
+        other_ext_student = UserFactory.create()
+        other_ext_student.username = "teststudent2"
+        other_ext_student.email = "teststudent2@other.edu"
+        other_ext_student.save()
+        extauth = ExternalAuthMap(external_id='testuser1@other.edu',
+                                  external_email='',
+                                  external_domain='shib:https://other.edu/',
+                                  external_credentials="",
+                                  user=other_ext_student)
+        extauth.save()
+
+        int_student = UserFactory.create()
+        int_student.username = "teststudent3"
+        int_student.email = "teststudent3@gmail.com"
+        int_student.save()
+
+        # Tests the two case for courses, limited and not
+        for course in [shib_course, open_enroll_course]:
+            for student in [shib_student, other_ext_student, int_student]:
+                request = self.request_factory.post('/change_enrollment')
+
+                request.POST.update({'enrollment_action': 'enroll',
+                                     'course_id': course.id.to_deprecated_string()})
+                request.user = student
+                response = change_enrollment(request)
+                # If course is not limited or student has correct shib extauth then enrollment should be allowed
+                if course is open_enroll_course or student is shib_student:
+                    self.assertEqual(response.status_code, 200)
+                    self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
+                else:
+                    self.assertEqual(response.status_code, 400)
+                    self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
+
+    @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set")
+    def test_shib_login_enrollment(self):
+        """
+            A functionality test that a student with an existing shib login
+            can auto-enroll in a class with GET or POST params.  Also tests the direction functionality of
+            the 'next' GET/POST param
+        """
+        student = UserFactory.create()
+        extauth = ExternalAuthMap(external_id='testuser@stanford.edu',
+                                  external_email='',
+                                  external_domain='shib:https://idp.stanford.edu/',
+                                  external_credentials="",
+                                  internal_password="password",
+                                  user=student)
+        student.set_password("password")
+        student.save()
+        extauth.save()
+
+        course = CourseFactory.create(
+            org='Stanford',
+            number='123',
+            display_name='Shib Only',
+            enrollment_domain='shib:https://idp.stanford.edu/',
+            user_id=self.test_user_id,
+        )
+
+        # use django test client for sessions and url processing
+        # no enrollment before trying
+        self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
+        self.client.logout()
+        params = [
+            ('course_id', course.id.to_deprecated_string()),
+            ('enrollment_action', 'enroll'),
+            ('next', '/testredirect')
+        ]
+        request_kwargs = {'path': '/shib-login/',
+                          'data': dict(params),
+                          'follow': False,
+                          'REMOTE_USER': 'testuser@stanford.edu',
+                          'Shib-Identity-Provider': 'https://idp.stanford.edu/'}
+        response = self.client.get(**request_kwargs)
+        # successful login is a redirect to the URL that handles auto-enrollment
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response['location'], 'http://testserver/account/finish_auth?{}'.format(urlencode(params)))
+
+
+class ShibUtilFnTest(TestCase):
+    """
+    Tests util functions in shib module
+    """
+    def test__flatten_to_ascii(self):
+        DIACRITIC = u"àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ"  # pylint: disable=invalid-name
+        STR_DIACRI = "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸåÅçÇ"  # pylint: disable=invalid-name
+        FLATTENED = u"aeiouAEIOUaeiouyAEIOUYaeiouAEIOUanoANOaeiouyAEIOUYaAcC"  # pylint: disable=invalid-name
+        self.assertEqual(_flatten_to_ascii('jasön'), 'jason')  # umlaut
+        self.assertEqual(_flatten_to_ascii('Jason包'), 'Jason')  # mandarin, so it just gets dropped
+        self.assertEqual(_flatten_to_ascii('abc'), 'abc')  # pass through
+
+        unicode_test = _flatten_to_ascii(DIACRITIC)
+        self.assertEqual(unicode_test, FLATTENED)
+        self.assertIsInstance(unicode_test, unicode)
+
+        str_test = _flatten_to_ascii(STR_DIACRI)
+        self.assertEqual(str_test, FLATTENED)
+        self.assertIsInstance(str_test, str)
diff --git a/openedx/core/djangoapps/external_auth/tests/test_ssl.py b/openedx/core/djangoapps/external_auth/tests/test_ssl.py
new file mode 100644
index 0000000..910a20e
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/tests/test_ssl.py
@@ -0,0 +1,418 @@
+"""
+Provides unit tests for SSL based authentication portions
+of the external_auth app.
+"""
+# pylint: disable=no-member
+import copy
+import unittest
+
+from contextlib import contextmanager
+from django.conf import settings
+from django.contrib.auth import SESSION_KEY
+from django.contrib.auth.models import AnonymousUser, User
+from django.contrib.sessions.middleware import SessionMiddleware
+from django.core.urlresolvers import reverse
+from django.test.client import Client
+from django.test.client import RequestFactory
+from django.test.utils import override_settings
+from mock import Mock, patch
+
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
+import openedx.core.djangoapps.external_auth.views as external_auth_views
+from student.models import CourseEnrollment
+from student.roles import CourseStaffRole
+from student.tests.factories import UserFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+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
+CACHES_ENABLE_GENERAL = copy.deepcopy(settings.CACHES)
+CACHES_ENABLE_GENERAL['general']['BACKEND'] = 'django.core.cache.backends.locmem.LocMemCache'
+
+
+@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH)
+@override_settings(CACHES=CACHES_ENABLE_GENERAL)
+class SSLClientTest(ModuleStoreTestCase):
+    """
+    Tests SSL Authentication code sections of external_auth
+    """
+
+    AUTH_DN = '/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}'
+    USER_NAME = 'test_user_ssl'
+    USER_EMAIL = 'test_user_ssl@EDX.ORG'
+    MOCK_URL = '/'
+
+    @contextmanager
+    def _create_ssl_request(self, url):
+        """Creates a basic request for SSL use."""
+        request = self.factory.get(url)
+        request.META['SSL_CLIENT_S_DN'] = self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
+        request.user = AnonymousUser()
+        middleware = SessionMiddleware()
+        middleware.process_request(request)
+        request.session.save()
+
+        with patch('edxmako.request_context.get_current_request', return_value=request):
+            yield request
+
+    @contextmanager
+    def _create_normal_request(self, url):
+        """Creates sessioned request without SSL headers"""
+        request = self.factory.get(url)
+        request.user = AnonymousUser()
+        middleware = SessionMiddleware()
+        middleware.process_request(request)
+        request.session.save()
+
+        with patch('edxmako.request_context.get_current_request', return_value=request):
+            yield request
+
+    def setUp(self):
+        """Setup test case by adding primary user."""
+        super(SSLClientTest, self).setUp()
+        self.client = Client()
+        self.factory = RequestFactory()
+        self.mock = Mock()
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    def test_ssl_login_with_signup_lms(self):
+        """
+        Validate that an SSL login creates an eamap user and
+        redirects them to the signup page.
+        """
+        with self._create_ssl_request('/') as request:
+            response = external_auth_views.ssl_login(request)
+
+        # Response should contain template for signup form, eamap should have user, and internal
+        # auth should not have a user
+        self.assertIn('<form role="form" id="register-form" method="post"', response.content)
+        try:
+            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
+
+        with self.assertRaises(User.DoesNotExist):
+            User.objects.get(email=self.USER_EMAIL)
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
+    def test_ssl_login_with_signup_cms(self):
+        """
+        Validate that an SSL login creates an eamap user and
+        redirects them to the signup page on CMS.
+        """
+        self.client.get(
+            reverse('contentstore.views.login_page'),
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
+        )
+
+        try:
+            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
+
+        with self.assertRaises(User.DoesNotExist):
+            User.objects.get(email=self.USER_EMAIL)
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_ssl_login_without_signup_lms(self):
+        """
+        Test IMMEDIATE_SIGNUP feature flag and ensure the user account is automatically created
+        and the user is redirected to slash.
+        """
+        with self._create_ssl_request('/') as request:
+            external_auth_views.ssl_login(request)
+
+        # Assert our user exists in both eamap and Users, and that we are logged in
+        try:
+            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
+        try:
+            User.objects.get(email=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to internal users, exception was {0}'.format(str(ex)))
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_ssl_login_without_signup_cms(self):
+        """
+        Test IMMEDIATE_SIGNUP feature flag and ensure the user account is
+        automatically created on CMS, and that we are redirected
+        to courses.
+        """
+
+        response = self.client.get(
+            reverse('contentstore.views.login_page'),
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
+        )
+        self.assertEqual(response.status_code, 302)
+        self.assertIn('/course', response['location'])
+
+        # Assert our user exists in both eamap and Users, and that we are logged in
+        try:
+            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
+        try:
+            User.objects.get(email=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to internal users, exception was {0}'.format(str(ex)))
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_default_login_decorator_ssl(self):
+        """
+        Make sure that SSL login happens if it is enabled on protected
+        views instead of showing the login form.
+        """
+        response = self.client.get(reverse('dashboard'), follows=True)
+        self.assertEqual(response.status_code, 302)
+        self.assertIn(reverse('signin_user'), response['location'])
+
+        response = self.client.get(
+            reverse('dashboard'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
+        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')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_registration_page_bypass(self):
+        """
+        This tests to make sure when immediate signup is on that
+        the user doesn't get presented with the registration page.
+        """
+        response = self.client.get(
+            reverse('register_user'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
+        self.assertEquals(('http://testserver/dashboard', 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_IMMEDIATE_SIGNUP)
+    def test_cms_registration_page_bypass(self):
+        """
+        This tests to make sure when immediate signup is on that
+        the user doesn't get presented with the registration page.
+        """
+        response = self.client.get(
+            reverse('signup'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
+        )
+        self.assertEqual(response.status_code, 404)
+        # assert that we are logged in
+        self.assertIn(SESSION_KEY, self.client.session)
+
+        # Now that we are logged in, make sure we don't see the registration page
+        response = self.client.get(reverse('signup'), follow=True)
+        self.assertEqual(response.status_code, 404)
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_signin_page_bypass(self):
+        """
+        This tests to make sure when ssl authentication is on
+        that user doesn't get presented with the login page if they
+        have a certificate.
+        """
+        # Test that they do signin if they don't have a cert
+        response = self.client.get(reverse('signin_user'))
+        self.assertEqual(200, response.status_code)
+        self.assertIn('login-and-registration-container', response.content)
+
+        # And get directly logged in otherwise
+        response = self.client.get(
+            reverse('signin_user'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
+        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')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_ssl_bad_eamap(self):
+        """
+        This tests the response when a user exists but their eamap
+        password doesn't match their internal password.
+
+        The internal password use for certificates has been removed
+        and this should not fail.
+        """
+        # Create account, break internal password, and activate account
+
+        with self._create_ssl_request('/') as request:
+            external_auth_views.ssl_login(request)
+        user = User.objects.get(email=self.USER_EMAIL)
+        user.set_password('not autogenerated')
+        user.is_active = True
+        user.save()
+
+        # Make sure we can still login
+        self.client.get(
+            reverse('signin_user'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
+        self.assertIn(SESSION_KEY, self.client.session)
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    @override_settings(FEATURES=FEATURES_WITHOUT_SSL_AUTH)
+    def test_ssl_decorator_no_certs(self):
+        """Make sure no external auth happens without SSL enabled"""
+
+        dec_mock = external_auth_views.ssl_login_shortcut(self.mock)
+
+        with self._create_normal_request(self.MOCK_URL) as request:
+            request.user = AnonymousUser()
+            # Call decorated mock function to make sure it passes
+            # the call through without hitting the external_auth functions and
+            # thereby creating an external auth map object.
+            dec_mock(request)
+        self.assertTrue(self.mock.called)
+        self.assertEqual(0, len(ExternalAuthMap.objects.all()))
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    def test_ssl_login_decorator(self):
+        """Create mock function to test ssl login decorator"""
+
+        dec_mock = external_auth_views.ssl_login_shortcut(self.mock)
+
+        # Test that anonymous without cert doesn't create authmap
+        with self._create_normal_request(self.MOCK_URL) as request:
+            dec_mock(request)
+        self.assertTrue(self.mock.called)
+        self.assertEqual(0, len(ExternalAuthMap.objects.all()))
+
+        # Test valid user
+        self.mock.reset_mock()
+        with self._create_ssl_request(self.MOCK_URL) as request:
+            dec_mock(request)
+        self.assertFalse(self.mock.called)
+        self.assertEqual(1, len(ExternalAuthMap.objects.all()))
+
+        # Test logged in user gets called
+        self.mock.reset_mock()
+        with self._create_ssl_request(self.MOCK_URL) as request:
+            request.user = UserFactory()
+            dec_mock(request)
+        self.assertTrue(self.mock.called)
+
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP)
+    def test_ssl_decorator_auto_signup(self):
+        """
+        Test that with auto signup the decorator
+        will bypass registration and call retfun.
+        """
+
+        dec_mock = external_auth_views.ssl_login_shortcut(self.mock)
+        with self._create_ssl_request(self.MOCK_URL) as request:
+            dec_mock(request)
+
+        # Assert our user exists in both eamap and Users
+        try:
+            ExternalAuthMap.objects.get(external_id=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to external auth map, exception was {0}'.format(str(ex)))
+        try:
+            User.objects.get(email=self.USER_EMAIL)
+        except ExternalAuthMap.DoesNotExist, ex:
+            self.fail('User did not get properly added to internal users, exception was {0}'.format(str(ex)))
+        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)
+    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'
+        )
+
+        with self._create_ssl_request('/') as request:
+            external_auth_views.ssl_login(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.assertNotIn(SESSION_KEY, 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'
+        )
+
+        with self._create_ssl_request('/') as request:
+            external_auth_views.ssl_login(request)
+        user = User.objects.get(email=self.USER_EMAIL)
+        CourseEnrollment.enroll(user, course.id)
+
+        CourseStaffRole(course.id).add_users(user)
+        course_private_url = reverse('course_handler', args=(unicode(course.id),))
+        self.assertNotIn(SESSION_KEY, 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 == 'lms.urls', 'Test only valid in lms')
+    @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE)
+    def test_ssl_logout(self):
+        """
+        Because the branding view is cached for anonymous users and we
+        use that to login users, the browser wasn't actually making the
+        request to that view as the redirect was being cached. This caused
+        a redirect loop, and this test confirms that that won't happen.
+
+        Test is only in LMS because we don't use / in studio to login SSL users.
+        """
+        response = self.client.get(
+            reverse('dashboard'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
+        self.assertEquals(('http://testserver/dashboard', 302),
+                          response.redirect_chain[-1])
+        self.assertIn(SESSION_KEY, self.client.session)
+        response = self.client.get(
+            reverse('logout'), follow=True,
+            SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
+        )
+        # Make sure that even though we logged out, we have logged back in
+        self.assertIn(SESSION_KEY, self.client.session)
diff --git a/openedx/core/djangoapps/external_auth/views.py b/openedx/core/djangoapps/external_auth/views.py
new file mode 100644
index 0000000..c38e8b7
--- /dev/null
+++ b/openedx/core/djangoapps/external_auth/views.py
@@ -0,0 +1,954 @@
+"""
+External Auth Views
+"""
+import functools
+import json
+import logging
+import random
+import re
+import string
+import fnmatch
+import unicodedata
+import urllib
+
+from textwrap import dedent
+from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
+from openedx.core.djangoapps.external_auth.djangostore import DjangoOpenIDStore
+
+from django.conf import settings
+from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.core.validators import validate_email
+from django.core.exceptions import ValidationError
+
+if settings.FEATURES.get('AUTH_USE_CAS'):
+    from django_cas.views import login as django_cas_login
+
+from student.helpers import get_next_url_for_login_page
+from student.models import UserProfile
+
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
+from django.utils.http import urlquote, is_safe_url
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+
+from edxmako.shortcuts import render_to_response, render_to_string
+from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
+
+import django_openid_auth.views as openid_views
+from django_openid_auth import auth as openid_auth
+from openid.consumer.consumer import SUCCESS
+
+from openid.server.server import Server, ProtocolError, UntrustedReturnURL
+from openid.server.trustroot import TrustRoot
+from openid.extensions import ax, sreg
+from ratelimitbackend.exceptions import RateLimitException
+
+import student.views
+from xmodule.modulestore.django import modulestore
+from opaque_keys.edx.locations import SlashSeparatedCourseKey
+
+log = logging.getLogger("edx.external_auth")
+AUDIT_LOG = logging.getLogger("audit")
+
+SHIBBOLETH_DOMAIN_PREFIX = settings.SHIBBOLETH_DOMAIN_PREFIX
+OPENID_DOMAIN_PREFIX = settings.OPENID_DOMAIN_PREFIX
+
+# -----------------------------------------------------------------------------
+# OpenID Common
+# -----------------------------------------------------------------------------
+
+
+@csrf_exempt
+def default_render_failure(request,  # pylint: disable=unused-argument
+                           message,
+                           status=403,
+                           template_name='extauth_failure.html',
+                           exception=None):
+    """Render an Openid error page to the user"""
+
+    log.debug("In openid_failure " + message)
+
+    data = render_to_string(template_name,
+                            dict(message=message, exception=exception))
+
+    return HttpResponse(data, status=status)
+
+
+# -----------------------------------------------------------------------------
+# OpenID Authentication
+# -----------------------------------------------------------------------------
+
+
+def generate_password(length=12, chars=string.letters + string.digits):
+    """Generate internal password for externally authenticated user"""
+    choice = random.SystemRandom().choice
+    return ''.join([choice(chars) for _i in range(length)])
+
+
+@csrf_exempt
+def openid_login_complete(request,
+                          redirect_field_name=REDIRECT_FIELD_NAME,  # pylint: disable=unused-argument
+                          render_failure=None):
+    """Complete the openid login process"""
+
+    render_failure = (render_failure or default_render_failure)
+
+    openid_response = openid_views.parse_openid_response(request)
+    if not openid_response:
+        return render_failure(request,
+                              'This is an OpenID relying party endpoint.')
+
+    if openid_response.status == SUCCESS:
+        external_id = openid_response.identity_url
+        oid_backend = openid_auth.OpenIDBackend()
+        details = oid_backend._extract_user_details(openid_response)  # pylint: disable=protected-access
+
+        log.debug('openid success, details=%s', details)
+
+        url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
+        external_domain = "{0}{1}".format(OPENID_DOMAIN_PREFIX, url)
+        fullname = '%s %s' % (details.get('first_name', ''),
+                              details.get('last_name', ''))
+
+        return _external_login_or_signup(
+            request,
+            external_id,
+            external_domain,
+            details,
+            details.get('email', ''),
+            fullname,
+            retfun=functools.partial(redirect, get_next_url_for_login_page(request)),
+        )
+
+    return render_failure(request, 'Openid failure')
+
+
+def _external_login_or_signup(request,
+                              external_id,
+                              external_domain,
+                              credentials,
+                              email,
+                              fullname,
+                              retfun=None):
+    """Generic external auth login or signup"""
+    # pylint: disable=too-many-statements
+    # see if we have a map from this external_id to an edX username
+    try:
+        eamap = ExternalAuthMap.objects.get(external_id=external_id,
+                                            external_domain=external_domain)
+        log.debug(u'Found eamap=%s', eamap)
+    except ExternalAuthMap.DoesNotExist:
+        # go render form for creating edX user
+        eamap = ExternalAuthMap(external_id=external_id,
+                                external_domain=external_domain,
+                                external_credentials=json.dumps(credentials))
+        eamap.external_email = email
+        eamap.external_name = fullname
+        eamap.internal_password = generate_password()
+        log.debug(u'Created eamap=%s', eamap)
+        eamap.save()
+
+    log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname)
+    uses_shibboleth = settings.FEATURES.get('AUTH_USE_SHIB') and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)
+    uses_certs = settings.FEATURES.get('AUTH_USE_CERTIFICATES')
+    internal_user = eamap.user
+    if internal_user is None:
+        if uses_shibboleth:
+            # If we are using shib, try to link accounts
+            # For Stanford shib, the email the idp returns is actually under the control of the user.
+            # Since the id the idps return is not user-editable, and is of the from "username@stanford.edu",
+            # use the id to link accounts instead.
+            try:
+                link_user = User.objects.get(email=eamap.external_id)
+                if not ExternalAuthMap.objects.filter(user=link_user).exists():
+                    # if there's no pre-existing linked eamap, we link the user
+                    eamap.user = link_user
+                    eamap.save()
+                    internal_user = link_user
+                    log.info(u'SHIB: Linking existing account for %s', eamap.external_id)
+                    # now pass through to log in
+                else:
+                    # otherwise, there must have been an error, b/c we've already linked a user with these external
+                    # creds
+                    failure_msg = _(
+                        "You have already created an account using "
+                        "an external login like WebAuth or Shibboleth. "
+                        "Please contact {tech_support_email} for support."
+                    ).format(
+                        tech_support_email=settings.TECH_SUPPORT_EMAIL,
+                    )
+                    return default_render_failure(request, failure_msg)
+            except User.DoesNotExist:
+                log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email)
+                return _signup(request, eamap, retfun)
+        else:
+            log.info(u'No user for %s yet. doing signup', eamap.external_email)
+            return _signup(request, eamap, retfun)
+
+    # We trust shib's authentication, so no need to authenticate using the password again
+    uname = internal_user.username
+    if uses_shibboleth:
+        user = internal_user
+        # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe
+        if settings.AUTHENTICATION_BACKENDS:
+            auth_backend = settings.AUTHENTICATION_BACKENDS[0]
+        else:
+            auth_backend = 'ratelimitbackend.backends.RateLimitModelBackend'
+        user.backend = auth_backend
+        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+            AUDIT_LOG.info(u'Linked user.id: {0} logged in via Shibboleth'.format(user.id))
+        else:
+            AUDIT_LOG.info(u'Linked user "{0}" logged in via Shibboleth'.format(user.email))
+    elif uses_certs:
+        # Certificates are trusted, so just link the user and log the action
+        user = internal_user
+        user.backend = 'ratelimitbackend.backends.RateLimitModelBackend'
+        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+            AUDIT_LOG.info(u'Linked user_id {0} logged in via SSL certificate'.format(user.id))
+        else:
+            AUDIT_LOG.info(u'Linked user "{0}" logged in via SSL certificate'.format(user.email))
+    else:
+        user = authenticate(username=uname, password=eamap.internal_password, request=request)
+    if user is None:
+        # we want to log the failure, but don't want to log the password attempted:
+        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+            AUDIT_LOG.warning(u'External Auth Login failed')
+        else:
+            AUDIT_LOG.warning(u'External Auth Login failed for "{0}"'.format(uname))
+        return _signup(request, eamap, retfun)
+
+    if not user.is_active:
+        if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
+            # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users
+            # that aren't already active
+            user.is_active = True
+            user.save()
+            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+                AUDIT_LOG.info(u'Activating user {0} due to external auth'.format(user.id))
+            else:
+                AUDIT_LOG.info(u'Activating user "{0}" due to external auth'.format(uname))
+        else:
+            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+                AUDIT_LOG.warning(u'User {0} is not active after external login'.format(user.id))
+            else:
+                AUDIT_LOG.warning(u'User "{0}" is not active after external login'.format(uname))
+            # TODO: improve error page
+            msg = 'Account not yet activated: please look for link in your email'
+            return default_render_failure(request, msg)
+
+    login(request, user)
+    request.session.set_expiry(0)
+
+    if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+        AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id))
+    else:
+        AUDIT_LOG.info(u"Login success - {0} ({1})".format(user.username, user.email))
+    if retfun is None:
+        return redirect('/')
+    return retfun()
+
+
+def _flatten_to_ascii(txt):
+    """
+    Flattens possibly unicode txt to ascii (django username limitation)
+    @param name:
+    @return: the flattened txt (in the same type as was originally passed in)
+    """
+    if isinstance(txt, str):
+        txt = txt.decode('utf-8')
+        return unicodedata.normalize('NFKD', txt).encode('ASCII', 'ignore')
+    else:
+        return unicode(unicodedata.normalize('NFKD', txt).encode('ASCII', 'ignore'))
+
+
+@ensure_csrf_cookie
+def _signup(request, eamap, retfun=None):
+    """
+    Present form to complete for signup via external authentication.
+    Even though the user has external credentials, he/she still needs
+    to create an account on the edX system, and fill in the user
+    registration form.
+
+    eamap is an ExternalAuthMap object, specifying the external user
+    for which to complete the signup.
+
+    retfun is a function to execute for the return value, if immediate
+    signup is used.  That allows @ssl_login_shortcut() to work.
+    """
+    # save this for use by student.views.create_account
+    request.session['ExternalAuthMap'] = eamap
+
+    if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP', ''):
+        # do signin immediately, by calling create_account, instead of asking
+        # student to fill in form.  MIT students already have information filed.
+        username = eamap.external_email.split('@', 1)[0]
+        username = username.replace('.', '_')
+        post_vars = dict(username=username,
+                         honor_code=u'true',
+                         terms_of_service=u'true')
+        log.info(u'doing immediate signup for %s, params=%s', username, post_vars)
+        student.views.create_account(request, post_vars)
+        # should check return content for successful completion before
+        if retfun is not None:
+            return retfun()
+        else:
+            return redirect('/')
+
+    # default conjoin name, no spaces, flattened to ascii b/c django can't handle unicode usernames, sadly
+    # but this only affects username, not fullname
+    username = re.sub(r'\s', '', _flatten_to_ascii(eamap.external_name), flags=re.UNICODE)
+
+    context = {
+        'has_extauth_info': True,
+        'show_signup_immediately': True,
+        'extauth_domain': eamap.external_domain,
+        'extauth_id': eamap.external_id,
+        'extauth_email': eamap.external_email,
+        'extauth_username': username,
+        'extauth_name': eamap.external_name,
+        'ask_for_tos': True,
+    }
+
+    # Some openEdX instances can't have terms of service for shib users, like
+    # according to Stanford's Office of General Counsel
+    uses_shibboleth = (settings.FEATURES.get('AUTH_USE_SHIB') and
+                       eamap.external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX))
+    if uses_shibboleth and settings.FEATURES.get('SHIB_DISABLE_TOS'):
+        context['ask_for_tos'] = False
+
+    # detect if full name is blank and ask for it from user
+    context['ask_for_fullname'] = eamap.external_name.strip() == ''
+
+    # validate provided mail and if it's not valid ask the user
+    try:
+        validate_email(eamap.external_email)
+        context['ask_for_email'] = False
+    except ValidationError:
+        context['ask_for_email'] = True
+
+    log.info(u'EXTAUTH: Doing signup for %s', eamap.external_id)
+
+    return student.views.register_user(request, extra_context=context)
+
+
+# -----------------------------------------------------------------------------
+# MIT SSL
+# -----------------------------------------------------------------------------
+
+
+def _ssl_dn_extract_info(dn_string):
+    """
+    Extract username, email address (may be anyuser@anydomain.com) and
+    full name from the SSL DN string.  Return (user,email,fullname) if
+    successful, and None otherwise.
+    """
+    search_string = re.search('/emailAddress=(.*)@([^/]+)', dn_string)
+    if search_string:
+        user = search_string.group(1)
+        email = "%s@%s" % (user, search_string.group(2))
+    else:
+        raise ValueError
+    search_string = re.search('/CN=([^/]+)/', dn_string)
+    if search_string:
+        fullname = search_string.group(1)
+    else:
+        raise ValueError
+    return (user, email, fullname)
+
+
+def ssl_get_cert_from_request(request):
+    """
+    Extract user information from certificate, if it exists, returning (user, email, fullname).
+    Else return None.
+    """
+    certkey = "SSL_CLIENT_S_DN"  # specify the request.META field to use
+
+    cert = request.META.get(certkey, '')
+    if not cert:
+        cert = request.META.get('HTTP_' + certkey, '')
+    if not cert:
+        try:
+            # try the direct apache2 SSL key
+            cert = request._req.subprocess_env.get(certkey, '')  # pylint: disable=protected-access
+        except Exception:  # pylint: disable=broad-except
+            return ''
+
+    return cert
+
+
+def ssl_login_shortcut(func):
+    """
+    Python function decorator for login procedures, to allow direct login
+    based on existing ExternalAuth record and MIT ssl certificate.
+    """
+    def wrapped(*args, **kwargs):
+        """
+        This manages the function wrapping, by determining whether to inject
+        the _external signup or just continuing to the internal function
+        call.
+        """
+
+        if not settings.FEATURES['AUTH_USE_CERTIFICATES']:
+            return func(*args, **kwargs)
+        request = args[0]
+
+        if request.user and request.user.is_authenticated():  # don't re-authenticate
+            return func(*args, **kwargs)
+
+        cert = ssl_get_cert_from_request(request)
+        if not cert:		# no certificate information - show normal login window
+            return func(*args, **kwargs)
+
+        def retfun():
+            """Wrap function again for call by _external_login_or_signup"""
+            return func(*args, **kwargs)
+
+        (_user, email, fullname) = _ssl_dn_extract_info(cert)
+        return _external_login_or_signup(
+            request,
+            external_id=email,
+            external_domain="ssl:MIT",
+            credentials=cert,
+            email=email,
+            fullname=fullname,
+            retfun=retfun
+        )
+    return wrapped
+
+
+@csrf_exempt
+def ssl_login(request):
+    """
+    This is called by branding.views.index when
+    FEATURES['AUTH_USE_CERTIFICATES'] = True
+
+    Used for MIT user authentication.  This presumes the web server
+    (nginx) has been configured to require specific client
+    certificates.
+
+    If the incoming protocol is HTTPS (SSL) then authenticate via
+    client certificate.  The certificate provides user email and
+    fullname; this populates the ExternalAuthMap.  The user is
+    nevertheless still asked to complete the edX signup.
+
+    Else continues on with student.views.index, and no authentication.
+    """
+    # Just to make sure we're calling this only at MIT:
+    if not settings.FEATURES['AUTH_USE_CERTIFICATES']:
+        return HttpResponseForbidden()
+
+    cert = ssl_get_cert_from_request(request)
+
+    if not cert:
+        # no certificate information - go onward to main index
+        return student.views.index(request)
+
+    (_user, email, fullname) = _ssl_dn_extract_info(cert)
+
+    redirect_to = get_next_url_for_login_page(request)
+    retfun = functools.partial(redirect, redirect_to)
+    return _external_login_or_signup(
+        request,
+        external_id=email,
+        external_domain="ssl:MIT",
+        credentials=cert,
+        email=email,
+        fullname=fullname,
+        retfun=retfun
+    )
+
+
+# -----------------------------------------------------------------------------
+# CAS (Central Authentication Service)
+# -----------------------------------------------------------------------------
+def cas_login(request, next_page=None, required=False):
+    """
+        Uses django_cas for authentication.
+        CAS is a common authentcation method pioneered by Yale.
+        See http://en.wikipedia.org/wiki/Central_Authentication_Service
+
+        Does normal CAS login then generates user_profile if nonexistent,
+        and if login was successful.  We assume that user details are
+        maintained by the central service, and thus an empty user profile
+        is appropriate.
+    """
+
+    ret = django_cas_login(request, next_page, required)
+
+    if request.user.is_authenticated():
+        user = request.user
+        UserProfile.objects.get_or_create(
+            user=user,
+            defaults={'name': user.username}
+        )
+
+    return ret
+
+
+# -----------------------------------------------------------------------------
+# Shibboleth (Stanford and others.  Uses *Apache* environment variables)
+# -----------------------------------------------------------------------------
+def shib_login(request):
+    """
+        Uses Apache's REMOTE_USER environment variable as the external id.
+        This in turn typically uses EduPersonPrincipalName
+        http://www.incommonfederation.org/attributesummary.html#eduPersonPrincipal
+        but the configuration is in the shibboleth software.
+    """
+    shib_error_msg = dedent(_(
+        """
+        Your university identity server did not return your ID information to us.
+        Please try logging in again.  (You may need to restart your browser.)
+        """))
+
+    if not request.META.get('REMOTE_USER'):
+        log.error(u"SHIB: no REMOTE_USER found in request.META")
+        return default_render_failure(request, shib_error_msg)
+    elif not request.META.get('Shib-Identity-Provider'):
+        log.error(u"SHIB: no Shib-Identity-Provider in request.META")
+        return default_render_failure(request, shib_error_msg)
+    else:
+        # If we get here, the user has authenticated properly
+        shib = {attr: request.META.get(attr, '').decode('utf-8')
+                for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider', 'displayName']}
+
+        # Clean up first name, last name, and email address
+        # TODO: Make this less hardcoded re: format, but split will work
+        # even if ";" is not present, since we are accessing 1st element
+        shib['sn'] = shib['sn'].split(";")[0].strip().capitalize()
+        shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize()
+
+    # TODO: should we be logging creds here, at info level?
+    log.info(u"SHIB creds returned: %r", shib)
+
+    fullname = shib['displayName'] if shib['displayName'] else u'%s %s' % (shib['givenName'], shib['sn'])
+
+    redirect_to = get_next_url_for_login_page(request)
+    retfun = functools.partial(_safe_postlogin_redirect, redirect_to, request.get_host())
+
+    return _external_login_or_signup(
+        request,
+        external_id=shib['REMOTE_USER'],
+        external_domain=SHIBBOLETH_DOMAIN_PREFIX + shib['Shib-Identity-Provider'],
+        credentials=shib,
+        email=shib['mail'],
+        fullname=fullname,
+        retfun=retfun
+    )
+
+
+def _safe_postlogin_redirect(redirect_to, safehost, default_redirect='/'):
+    """
+    If redirect_to param is safe (not off this host), then perform the redirect.
+    Otherwise just redirect to '/'.
+    Basically copied from django.contrib.auth.views.login
+    @param redirect_to: user-supplied redirect url
+    @param safehost: which host is safe to redirect to
+    @return: an HttpResponseRedirect
+    """
+    if is_safe_url(url=redirect_to, host=safehost):
+        return redirect(redirect_to)
+    return redirect(default_redirect)
+
+
+def course_specific_login(request, course_id):
+    """
+       Dispatcher function for selecting the specific login method
+       required by the course
+    """
+    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
+    course = modulestore().get_course(course_key)
+    if not course:
+        # couldn't find the course, will just return vanilla signin page
+        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 and
+            course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)
+    ):
+        return redirect_with_get('shib-login', request.GET)
+
+    # Default fallthrough to normal signin page
+    return redirect_with_get('signin_user', request.GET)
+
+
+def course_specific_register(request, course_id):
+    """
+        Dispatcher function for selecting the specific registration method
+        required by the course
+    """
+    course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
+    course = modulestore().get_course(course_key)
+
+    if not course:
+        # couldn't find the course, will just return vanilla registration page
+        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 and
+            course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)
+    ):
+        # shib-login takes care of both registration and login flows
+        return redirect_with_get('shib-login', request.GET)
+
+    # Default fallthrough to normal registration page
+    return redirect_with_get('register_user', request.GET)
+
+
+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" % (url, get_querydict.urlencode(safe='/')))
+    return redirect(view_name)
+
+
+# -----------------------------------------------------------------------------
+# OpenID Provider
+# -----------------------------------------------------------------------------
+
+
+def get_xrds_url(resource, request):
+    """
+    Return the XRDS url for a resource
+    """
+    host = request.get_host()
+
+    location = host + '/openid/provider/' + resource + '/'
+
+    if request.is_secure():
+        return 'https://' + location
+    else:
+        return 'http://' + location
+
+
+def add_openid_simple_registration(request, response, data):
+    """
+    Add simple registration fields to the response if requested.
+    """
+    sreg_data = {}
+    sreg_request = sreg.SRegRequest.fromOpenIDRequest(request)
+    sreg_fields = sreg_request.allRequestedFields()
+
+    # if consumer requested simple registration fields, add them
+    if sreg_fields:
+        for field in sreg_fields:
+            if field == 'email' and 'email' in data:
+                sreg_data['email'] = data['email']
+            elif field == 'fullname' and 'fullname' in data:
+                sreg_data['fullname'] = data['fullname']
+            elif field == 'nickname' and 'nickname' in data:
+                sreg_data['nickname'] = data['nickname']
+
+        # construct sreg response
+        sreg_response = sreg.SRegResponse.extractResponse(sreg_request,
+                                                          sreg_data)
+        sreg_response.toMessage(response.fields)
+
+
+def add_openid_attribute_exchange(request, response, data):
+    """
+    Add attribute exchange fields to the response if requested.
+    """
+    try:
+        ax_request = ax.FetchRequest.fromOpenIDRequest(request)
+    except ax.AXError:
+        #  not using OpenID attribute exchange extension
+        pass
+    else:
+        ax_response = ax.FetchResponse()
+
+        # if consumer requested attribute exchange fields, add them
+        if ax_request and ax_request.requested_attributes:
+            for type_uri in ax_request.requested_attributes.iterkeys():
+                email_schema = 'http://axschema.org/contact/email'
+                name_schema = 'http://axschema.org/namePerson'
+                if type_uri == email_schema and 'email' in data:
+                    ax_response.addValue(email_schema, data['email'])
+                elif type_uri == name_schema and 'fullname' in data:
+                    ax_response.addValue(name_schema, data['fullname'])
+
+            # construct ax response
+            ax_response.toMessage(response.fields)
+
+
+def provider_respond(server, request, response, data):
+    """
+    Respond to an OpenID request
+    """
+    # get and add extensions
+    add_openid_simple_registration(request, response, data)
+    add_openid_attribute_exchange(request, response, data)
+
+    # create http response from OpenID response
+    webresponse = server.encodeResponse(response)
+    http_response = HttpResponse(webresponse.body)
+    http_response.status_code = webresponse.code
+
+    # add OpenID headers to response
+    for key, val in webresponse.headers.iteritems():
+        http_response[key] = val
+
+    return http_response
+
+
+def validate_trust_root(openid_request):
+    """
+    Only allow OpenID requests from valid trust roots
+    """
+
+    trusted_roots = getattr(settings, 'OPENID_PROVIDER_TRUSTED_ROOT', None)
+
+    if not trusted_roots:
+        # not using trusted roots
+        return True
+
+    # don't allow empty trust roots
+    if (not hasattr(openid_request, 'trust_root') or
+            not openid_request.trust_root):
+        log.error('no trust_root')
+        return False
+
+    # ensure trust root parses cleanly (one wildcard, of form *.foo.com, etc.)
+    trust_root = TrustRoot.parse(openid_request.trust_root)
+    if not trust_root:
+        log.error('invalid trust_root')
+        return False
+
+    # don't allow empty return tos
+    if (not hasattr(openid_request, 'return_to') or
+            not openid_request.return_to):
+        log.error('empty return_to')
+        return False
+
+    # ensure return to is within trust root
+    if not trust_root.validateURL(openid_request.return_to):
+        log.error('invalid return_to')
+        return False
+
+    # check that the root matches the ones we trust
+    if not any(r for r in trusted_roots if fnmatch.fnmatch(trust_root, r)):
+        log.error('non-trusted root')
+        return False
+
+    return True
+
+
+@csrf_exempt
+def provider_login(request):
+    """
+    OpenID login endpoint
+    """
+    # pylint: disable=too-many-statements
+    # make and validate endpoint
+    endpoint = get_xrds_url('login', request)
+    if not endpoint:
+        return default_render_failure(request, "Invalid OpenID request")
+
+    # initialize store and server
+    store = DjangoOpenIDStore()
+    server = Server(store, endpoint)
+
+    # first check to see if the request is an OpenID request.
+    # If so, the client will have specified an 'openid.mode' as part
+    # of the request.
+    if request.method == 'GET':
+        querydict = dict(request.GET.items())
+    else:
+        querydict = dict(request.POST.items())
+    error = False
+    if 'openid.mode' in request.GET or 'openid.mode' in request.POST:
+        # decode request
+        try:
+            openid_request = server.decodeRequest(querydict)
+        except (UntrustedReturnURL, ProtocolError):
+            openid_request = None
+
+        if not openid_request:
+            return default_render_failure(request, "Invalid OpenID request")
+
+        # don't allow invalid and non-trusted trust roots
+        if not validate_trust_root(openid_request):
+            return default_render_failure(request, "Invalid OpenID trust root")
+
+        # checkid_immediate not supported, require user interaction
+        if openid_request.mode == 'checkid_immediate':
+            return provider_respond(server, openid_request,
+                                    openid_request.answer(False), {})
+
+        # checkid_setup, so display login page
+        # (by falling through to the provider_login at the
+        # bottom of this method).
+        elif openid_request.mode == 'checkid_setup':
+            if openid_request.idSelect():
+                # remember request and original path
+                request.session['openid_setup'] = {
+                    'request': openid_request,
+                    'url': request.get_full_path(),
+                    'post_params': request.POST,
+                }
+
+                # user failed login on previous attempt
+                if 'openid_error' in request.session:
+                    error = True
+                    del request.session['openid_error']
+
+        # OpenID response
+        else:
+            return provider_respond(server, openid_request,
+                                    server.handleRequest(openid_request), {})
+
+    # handle login redirection:  these are also sent to this view function,
+    # but are distinguished by lacking the openid mode.  We also know that
+    # they are posts, because they come from the popup
+    elif request.method == 'POST' and 'openid_setup' in request.session:
+        # get OpenID request from session
+        openid_setup = request.session['openid_setup']
+        openid_request = openid_setup['request']
+        openid_request_url = openid_setup['url']
+        post_params = openid_setup['post_params']
+        # We need to preserve the parameters, and the easiest way to do this is
+        # through the URL
+        url_post_params = {
+            param: post_params[param] for param in post_params if param.startswith('openid')
+        }
+
+        encoded_params = urllib.urlencode(url_post_params)
+
+        if '?' not in openid_request_url:
+            openid_request_url = openid_request_url + '?' + encoded_params
+        else:
+            openid_request_url = openid_request_url + '&' + encoded_params
+
+        del request.session['openid_setup']
+
+        # don't allow invalid trust roots
+        if not validate_trust_root(openid_request):
+            return default_render_failure(request, "Invalid OpenID trust root")
+
+        # check if user with given email exists
+        # Failure is redirected to this method (by using the original URL),
+        # which will bring up the login dialog.
+        email = request.POST.get('email', None)
+        try:
+            user = User.objects.get(email=email)
+        except User.DoesNotExist:
+            request.session['openid_error'] = True
+            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+                AUDIT_LOG.warning(u"OpenID login failed - Unknown user email")
+            else:
+                msg = u"OpenID login failed - Unknown user email: {0}".format(email)
+                AUDIT_LOG.warning(msg)
+            return HttpResponseRedirect(openid_request_url)
+
+        # attempt to authenticate user (but not actually log them in...)
+        # Failure is again redirected to the login dialog.
+        username = user.username
+        password = request.POST.get('password', None)
+        try:
+            user = authenticate(username=username, password=password, request=request)
+        except RateLimitException:
+            AUDIT_LOG.warning(u'OpenID - Too many failed login attempts.')
+            return HttpResponseRedirect(openid_request_url)
+
+        if user is None:
+            request.session['openid_error'] = True
+            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+                AUDIT_LOG.warning(u"OpenID login failed - invalid password")
+            else:
+                AUDIT_LOG.warning(
+                    u"OpenID login failed - password for %s is invalid", email)
+            return HttpResponseRedirect(openid_request_url)
+
+        # authentication succeeded, so fetch user information
+        # that was requested
+        if user is not None and user.is_active:
+            # remove error from session since login succeeded
+            if 'openid_error' in request.session:
+                del request.session['openid_error']
+
+            if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+                AUDIT_LOG.info(u"OpenID login success - user.id: %s", user.id)
+            else:
+                AUDIT_LOG.info(
+                    u"OpenID login success - %s (%s)", user.username, user.email)
+            # redirect user to return_to location
+            url = endpoint + urlquote(user.username)
+            response = openid_request.answer(True, None, url)
+
+            # Note too that this is hardcoded, and not really responding to
+            # the extensions that were registered in the first place.
+            results = {
+                'nickname': user.username,
+                'email': user.email,
+                'fullname': user.profile.name,
+            }
+
+            # the request succeeded:
+            return provider_respond(server, openid_request, response, results)
+
+        # the account is not active, so redirect back to the login page:
+        request.session['openid_error'] = True
+        if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
+            AUDIT_LOG.warning(
+                u"Login failed - Account not active for user.id %s", user.id)
+        else:
+            AUDIT_LOG.warning(
+                u"Login failed - Account not active for user %s", username)
+        return HttpResponseRedirect(openid_request_url)
+
+    # determine consumer domain if applicable
+    return_to = request.GET.get('openid.return_to') or request.POST.get('openid.return_to') or ''
+    if return_to:
+        matches = re.match(r'\w+:\/\/([\w\.-]+)', return_to)
+        return_to = matches.group(1)
+
+    # display login page
+    response = render_to_response('provider_login.html', {
+        'error': error,
+        'return_to': return_to
+    })
+
+    # add custom XRDS header necessary for discovery process
+    response['X-XRDS-Location'] = get_xrds_url('xrds', request)
+    return response
+
+
+def provider_identity(request):
+    """
+    XRDS for identity discovery
+    """
+
+    response = render_to_response('identity.xml',
+                                  {'url': get_xrds_url('login', request)},
+                                  content_type='text/xml')
+
+    # custom XRDS header necessary for discovery process
+    response['X-XRDS-Location'] = get_xrds_url('identity', request)
+    return response
+
+
+def provider_xrds(request):
+    """
+    XRDS for endpoint discovery
+    """
+
+    response = render_to_response('xrds.xml',
+                                  {'url': get_xrds_url('login', request)},
+                                  content_type='text/xml')
+
+    # custom XRDS header necessary for discovery process
+    response['X-XRDS-Location'] = get_xrds_url('xrds', request)
+    return response
--
libgit2 0.26.0