diff --git a/common/djangoapps/third_party_auth/__init__.py b/common/djangoapps/third_party_auth/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f9c56906c192f257f6156493b8c4f11b0bfdd7e
--- /dev/null
+++ b/common/djangoapps/third_party_auth/pipeline.py
@@ -0,0 +1,9 @@
+"""Auth pipeline definitions."""
+
+from social.pipeline import partial
+
+
+@partial.partial
+def step(*args, **kwargs):
+    """Fake pipeline step; just throws loudly for now."""
+    raise NotImplementedError('%s, %s' % (args, kwargs))
diff --git a/common/djangoapps/third_party_auth/provider.py b/common/djangoapps/third_party_auth/provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b1e17796d436fe28f524ca79c38072de5e56126
--- /dev/null
+++ b/common/djangoapps/third_party_auth/provider.py
@@ -0,0 +1,125 @@
+"""Third-party auth provider definitions.
+
+Loaded by Django's settings mechanism. Consequently, this module must not
+invoke the Django armature.
+"""
+
+
+class BaseProvider(object):
+    """Abstract base class for third-party auth providers.
+
+    All providers must subclass BaseProvider -- otherwise, they cannot be put
+    in the provider Registry.
+    """
+
+    # String. Dot-delimited module.Class. The name of the backend
+    # implementation to load.
+    AUTHENTICATION_BACKEND = None
+    # String. User-facing name of the provider. Must be unique across all
+    # enabled providers.
+    NAME = None
+
+    # Dict of string -> object. Settings that will be merged into Django's
+    # settings instance. In most cases the value will be None, since real
+    # values are merged from .json files (foo.auth.json; foo.env.json) onto the
+    # settings instance during application initialization.
+    SETTINGS = {}
+
+    @classmethod
+    def merge_onto(cls, settings):
+        """Merge class-level settings onto a django `settings` module."""
+        for key, value in cls.SETTINGS.iteritems():
+            setattr(settings, key, value)
+
+
+class GoogleOauth2(BaseProvider):
+    """Provider for Google's Oauth2 auth system."""
+
+    AUTHENTICATION_BACKEND = 'social.backends.google.GoogleOAuth2'
+    NAME = 'Google'
+    SETTINGS = {
+        'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': None,
+        'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': None,
+    }
+
+
+class LinkedInOauth2(BaseProvider):
+    """Provider for LinkedIn's Oauth2 auth system."""
+
+    AUTHENTICATION_BACKEND = 'social.backends.linkedin.LinkedinOAuth2'
+    NAME = 'LinkedIn'
+    SETTINGS = {
+        'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': None,
+        'SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET': None,
+    }
+
+
+class MozillaPersona(BaseProvider):
+    """Provider for Mozilla's Persona auth system."""
+
+    AUTHENTICATION_BACKEND = 'social.backends.persona.PersonaAuth'
+    NAME = 'Mozilla Persona'
+
+
+class Registry(object):
+    """Singleton registry of third-party auth providers.
+
+    Providers must subclass BaseProvider in order to be usable in the registry.
+    """
+
+    _CONFIGURED = False
+    _ENABLED = {}
+
+    @classmethod
+    def _check_configured(cls):
+        """Ensures registry is configured."""
+        if not cls._CONFIGURED:
+            raise RuntimeError('Registry not configured')
+
+    @classmethod
+    def _get_all(cls):
+        """Gets all provider implementations loaded into the Python runtime."""
+        # BaseProvider does so have __subclassess__. pylint: disable-msg=no-member
+        return {klass.NAME: klass for klass in BaseProvider.__subclasses__()}
+
+    @classmethod
+    def _enable(cls, provider):
+        """Enables a single `provider`."""
+        if provider.NAME in cls._ENABLED:
+            raise ValueError('Provider %s already enabled' % provider.NAME)
+        cls._ENABLED[provider.NAME] = provider
+
+    @classmethod
+    def configure_once(cls, provider_names):
+        """Configures providers.
+
+        Takes `provider_names`, a list of string.
+        """
+        if cls._CONFIGURED:
+            raise ValueError('Provider registry already configured')
+        # Flip the bit eagerly -- configure() should not be re-callable if one
+        # _enable call fails.
+        cls._CONFIGURED = True
+        for name in provider_names:
+            all_providers = cls._get_all()
+            if name not in all_providers:
+                raise ValueError('No implementation found for provider ' + name)
+            cls._enable(all_providers.get(name))
+
+    @classmethod
+    def enabled(cls):
+        """Returns list of enabled providers."""
+        cls._check_configured()
+        return sorted(cls._ENABLED.values(), key=lambda provider: provider.NAME)
+
+    @classmethod
+    def get(cls, provider_name):
+        """Gets provider named `provider_name` string if enabled, else None."""
+        cls._check_configured()
+        return cls._ENABLED.get(provider_name)
+
+    @classmethod
+    def _reset(cls):
+        """Returns the registry to an unconfigured state; for tests only."""
+        cls._CONFIGURED = False
+        cls._ENABLED = {}
diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..0ff55e1213d8b9eb27a184ba9aae0ec49603a00a
--- /dev/null
+++ b/common/djangoapps/third_party_auth/settings.py
@@ -0,0 +1,106 @@
+"""Settings for the third-party auth module.
+
+Defers configuration of settings so we can inspect the provider registry and
+create settings placeholders for only those values actually needed by a given
+deployment. Required by Django; consequently, this file must not invoke the
+Django armature.
+
+The flow for settings registration is:
+
+The base settings file contains a boolean, ENABLE_THIRD_PARTY_AUTH, indicating
+whether this module is enabled. Ancillary settings files (aws.py, dev.py) put
+options in THIRD_PARTY_SETTINGS. startup.py probes the ENABLE_THIRD_PARTY_AUTH.
+If true, it:
+
+    a) loads this module.
+    b) calls apply_settings(), passing in settings.THIRD_PARTY_AUTH.
+       THIRD_PARTY AUTH is a dict of the form
+
+       'THIRD_PARTY_AUTH': {
+            '<PROVIDER_NAME>': {
+                '<PROVIDER_SETTING_NAME>': '<PROVIDER_SETTING_VALUE>',
+                [...]
+            },
+            [...]
+       }
+
+       If you are using a dev settings file, your settings dict starts at the
+       level of <PROVIDER_NAME> and is a map of provider name string to
+       settings dict. If you are using an auth.json file, it should contain a
+       THIRD_PARTY_AUTH entry as above.
+    c) apply_settings() builds a list of <PROVIDER_NAMES>. These are the
+       enabled third party auth providers for the deployment. These are enabled
+       in provider.Registry, the canonical list of enabled providers.
+    d) then, it sets global, provider-independent settings.
+    e) then, it sets provider-specific settings. For each enabled provider, we
+       read its SETTINGS member. These are merged onto the Django settings
+       object. In most cases these are stubs and the real values are set from
+       THIRD_PARTY_AUTH. All values that are set from this dict must first be
+       initialized from SETTINGS. This allows us to validate the dict and
+       ensure that the values match expected configuration options on the
+       provider.
+    f) finally, the (key, value) pairs from the dict file are merged onto the
+       django settings object.
+"""
+
+from . import provider
+
+
+def _merge_auth_info(django_settings, auth_info):
+    """Merge `auth_info` dict onto `django_settings` module."""
+    enabled_provider_names = []
+    to_merge = []
+
+    for provider_name, provider_dict in auth_info.items():
+        enabled_provider_names.append(provider_name)
+        # Merge iff all settings have been intialized.
+        for key in provider_dict:
+            if key not in dir(django_settings):
+                raise ValueError('Auth setting %s not initialized' % key)
+        to_merge.append(provider_dict)
+
+    for passed_validation in to_merge:
+        for key, value in passed_validation.iteritems():
+            setattr(django_settings, key, value)
+
+
+def _set_global_settings(django_settings):
+    """Set provider-independent settings."""
+    # Register and configure python-social-auth with Django.
+    django_settings.INSTALLED_APPS += (
+        'social.apps.django_app.default',
+        'third_party_auth',
+    )
+    django_settings.TEMPLATE_CONTEXT_PROCESSORS += (
+        'social.apps.django_app.context_processors.backends',
+        'social.apps.django_app.context_processors.login_redirect',
+    )
+    # Inject our customized auth pipeline. All auth backends must work with
+    # this pipeline.
+    django_settings.SOCIAL_AUTH_PIPELINE = (
+        'third_party_auth.pipeline.step',
+    )
+
+
+def _set_provider_settings(django_settings, enabled_providers, auth_info):
+    """Set provider-specific settings."""
+    # Must prepend here so we get called first.
+    django_settings.AUTHENTICATION_BACKENDS = (
+        tuple(enabled_provider.AUTHENTICATION_BACKEND for enabled_provider in enabled_providers) +
+        django_settings.AUTHENTICATION_BACKENDS)
+
+    # Merge settings from provider classes, and configure all placeholders.
+    for enabled_provider in enabled_providers:
+        enabled_provider.merge_onto(django_settings)
+
+    # Merge settings from <deployment>.auth.json.
+    _merge_auth_info(django_settings, auth_info)
+
+
+def apply_settings(auth_info, django_settings):
+    """Apply settings from `auth_info` dict to `django_settings` module."""
+    provider_names = auth_info.keys()
+    provider.Registry.configure_once(provider_names)
+    enabled_providers = provider.Registry.enabled()
+    _set_global_settings(django_settings)
+    _set_provider_settings(django_settings, enabled_providers, auth_info)
diff --git a/common/djangoapps/third_party_auth/tests/__init__.py b/common/djangoapps/third_party_auth/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/common/djangoapps/third_party_auth/tests/test_provider.py b/common/djangoapps/third_party_auth/tests/test_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..18774eb61a7404b673a214b3bd0bd203af93510f
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/test_provider.py
@@ -0,0 +1,71 @@
+"""
+Test configuration of providers.
+"""
+
+from third_party_auth import provider
+from third_party_auth.tests import testutil
+
+
+class RegistryTest(testutil.TestCase):
+    """Tests registry discovery and operation."""
+
+    # Allow access to protected methods (or module-protected methods) under
+    # test.
+    # pylint: disable-msg=protected-access
+
+    def test_calling_configure_once_twice_raises_value_error(self):
+        provider.Registry.configure_once([provider.GoogleOauth2.NAME])
+
+        with self.assertRaisesRegexp(ValueError, '^.*already configured$'):
+            provider.Registry.configure_once([provider.GoogleOauth2.NAME])
+
+    def test_configure_once_adds_gettable_providers(self):
+        provider.Registry.configure_once([provider.GoogleOauth2.NAME])
+        self.assertIs(provider.GoogleOauth2, provider.Registry.get(provider.GoogleOauth2.NAME))
+
+    def test_configuring_provider_with_no_implementation_raises_value_error(self):
+        with self.assertRaisesRegexp(ValueError, '^.*no_implementation$'):
+            provider.Registry.configure_once(['no_implementation'])
+
+    def test_configuring_single_provider_twice_raises_value_error(self):
+        provider.Registry._enable(provider.GoogleOauth2)
+
+        with self.assertRaisesRegexp(ValueError, '^.*already enabled'):
+            provider.Registry.configure_once([provider.GoogleOauth2.NAME])
+
+    def test_custom_provider_can_be_enabled(self):
+        name = 'CustomProvider'
+
+        with self.assertRaisesRegexp(ValueError, '^No implementation.*$'):
+            provider.Registry.configure_once([name])
+
+        class CustomProvider(provider.BaseProvider):
+            """Custom class to ensure BaseProvider children outside provider can be enabled."""
+
+            NAME = name
+
+        provider.Registry._reset()
+        provider.Registry.configure_once([CustomProvider.NAME])
+        self.assertEqual([CustomProvider], provider.Registry.enabled())
+
+    def test_enabled_raises_runtime_error_if_not_configured(self):
+        with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'):
+            provider.Registry.enabled()
+
+    def test_enabled_returns_list_of_enabled_providers_sorted_by_name(self):
+        all_providers = provider.Registry._get_all()
+        provider.Registry.configure_once(all_providers.keys())
+        self.assertEqual(
+            sorted(all_providers.values(), key=lambda provider: provider.NAME), provider.Registry.enabled())
+
+    def test_get_raises_runtime_error_if_not_configured(self):
+        with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'):
+            provider.Registry.get('anything')
+
+    def test_get_returns_enabled_provider(self):
+        provider.Registry.configure_once([provider.GoogleOauth2.NAME])
+        self.assertIs(provider.GoogleOauth2, provider.Registry.get(provider.GoogleOauth2.NAME))
+
+    def test_get_returns_none_if_provider_not_enabled(self):
+        provider.Registry.configure_once([])
+        self.assertIsNone(provider.Registry.get(provider.MozillaPersona.NAME))
diff --git a/common/djangoapps/third_party_auth/tests/test_settings.py b/common/djangoapps/third_party_auth/tests/test_settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..05f502928430d94aa2bbd5f7cfb5881894f018e6
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/test_settings.py
@@ -0,0 +1,68 @@
+"""
+Unit tests for settings code.
+"""
+
+from third_party_auth import provider
+from third_party_auth import settings
+from third_party_auth.tests import testutil
+
+
+_ORIGINAL_AUTHENTICATION_BACKENDS = ('first_authentication_backend',)
+_ORIGINAL_INSTALLED_APPS = ('first_installed_app',)
+_ORIGINAL_TEMPLATE_CONTEXT_PROCESSORS = ('first_template_context_preprocessor',)
+_SETTINGS_MAP = {
+    'AUTHENTICATION_BACKENDS': _ORIGINAL_AUTHENTICATION_BACKENDS,
+    'INSTALLED_APPS': _ORIGINAL_INSTALLED_APPS,
+    'TEMPLATE_CONTEXT_PROCESSORS': _ORIGINAL_TEMPLATE_CONTEXT_PROCESSORS,
+}
+
+
+class SettingsUnitTest(testutil.TestCase):
+    """Unit tests for settings management code."""
+
+    # Suppress sprurious no-member warning on fakes.
+    # pylint: disable-msg=no-member
+
+    def setUp(self):
+        super(SettingsUnitTest, self).setUp()
+        self.settings = testutil.FakeDjangoSettings(_SETTINGS_MAP)
+
+    def test_apply_settings_adds_third_party_auth_to_installed_apps(self):
+        settings.apply_settings({}, self.settings)
+        self.assertIn('third_party_auth', self.settings.INSTALLED_APPS)
+
+    def test_apply_settings_enables_no_providers_and_completes_when_app_info_empty(self):
+        settings.apply_settings({}, self.settings)
+        self.assertEqual([], provider.Registry.enabled())
+
+    def test_apply_settings_initializes_stubs_and_merges_settings_from_auth_info(self):
+        for key in provider.GoogleOauth2.SETTINGS:
+            self.assertFalse(hasattr(self.settings, key))
+
+        auth_info = {
+            provider.GoogleOauth2.NAME: {
+                'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_oauth2_key',
+            },
+        }
+        settings.apply_settings(auth_info, self.settings)
+        self.assertEqual('google_oauth2_key', self.settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY)
+        self.assertIsNone(self.settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET)
+
+    def test_apply_settings_prepends_auth_backends(self):
+        self.assertEqual(_ORIGINAL_AUTHENTICATION_BACKENDS, self.settings.AUTHENTICATION_BACKENDS)
+        settings.apply_settings({provider.GoogleOauth2.NAME: {}, provider.MozillaPersona.NAME: {}}, self.settings)
+        self.assertEqual((
+            provider.GoogleOauth2.AUTHENTICATION_BACKEND, provider.MozillaPersona.AUTHENTICATION_BACKEND) +
+            _ORIGINAL_AUTHENTICATION_BACKENDS,
+            self.settings.AUTHENTICATION_BACKENDS)
+
+    def test_apply_settings_raises_value_error_if_provider_contains_uninitialized_setting(self):
+        bad_setting_name = 'bad_setting'
+        self.assertNotIn('bad_setting_name', provider.GoogleOauth2.SETTINGS)
+        auth_info = {
+            provider.GoogleOauth2.NAME: {
+                bad_setting_name: None,
+            },
+        }
+        with self.assertRaisesRegexp(ValueError, '^.*not initialized$'):
+            settings.apply_settings(auth_info, self.settings)
diff --git a/common/djangoapps/third_party_auth/tests/test_settings_integration.py b/common/djangoapps/third_party_auth/tests/test_settings_integration.py
new file mode 100644
index 0000000000000000000000000000000000000000..15cff5605a18f0843d79547cadbb4a652621759f
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/test_settings_integration.py
@@ -0,0 +1,39 @@
+"""
+Integration tests for settings code.
+"""
+
+import mock
+import unittest
+
+from django.conf import settings
+
+from third_party_auth import provider
+from third_party_auth import settings as auth_settings
+from third_party_auth.tests import testutil
+
+_AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH'
+
+
+class SettingsIntegrationTest(testutil.TestCase):
+    """Integration tests of auth settings pipeline."""
+
+    @unittest.skipUnless(_AUTH_FEATURES_KEY in settings.FEATURES, _AUTH_FEATURES_KEY + ' not in settings.FEATURES')
+    def test_enable_third_party_auth_is_disabled_by_default(self):
+        self.assertIs(False, settings.FEATURES.get(_AUTH_FEATURES_KEY))
+
+    @mock.patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_AUTH': True})
+    def test_can_enable_google_oauth2(self):
+        auth_settings.apply_settings({'Google': {'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_key'}}, settings)
+        self.assertEqual([provider.GoogleOauth2], provider.Registry.enabled())
+        self.assertEqual('google_key', settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY)
+
+    @mock.patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_AUTH': True})
+    def test_can_enable_linkedin_oauth2(self):
+        auth_settings.apply_settings({'LinkedIn': {'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': 'linkedin_key'}}, settings)
+        self.assertEqual([provider.LinkedInOauth2], provider.Registry.enabled())
+        self.assertEqual('linkedin_key', settings.SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY)
+
+    @mock.patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_ATUH': True})
+    def test_can_enable_mozilla_persona(self):
+        auth_settings.apply_settings({'Mozilla Persona': {}}, settings)
+        self.assertEqual([provider.MozillaPersona], provider.Registry.enabled())
diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py
new file mode 100644
index 0000000000000000000000000000000000000000..a431aaca02fd41425f1bcefc251dd2a6ae7ba998
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/testutil.py
@@ -0,0 +1,34 @@
+"""
+Utilities for writing third_party_auth tests.
+
+Used by Django and non-Django tests; must not have Django deps.
+"""
+
+import unittest
+
+from third_party_auth import provider
+
+
+class FakeDjangoSettings(object):
+    """A fake for Django settings."""
+
+    def __init__(self, mappings):
+        """Initializes the fake from `mappings`, a dict."""
+        for key, value in mappings.iteritems():
+            setattr(self, key, value)
+
+
+class TestCase(unittest.TestCase):
+    """Base class for auth test cases."""
+
+    # Allow access to protected methods (or module-protected methods) under
+    # test.
+    # pylint: disable-msg=protected-access
+
+    def setUp(self):
+        super(TestCase, self).setUp()
+        provider.Registry._reset()
+
+    def tearDown(self):
+        provider.Registry._reset()
+        super(TestCase, self).tearDown()
diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..05322a680d2a08fe7cefe3c1c76114111fdb104c
--- /dev/null
+++ b/common/djangoapps/third_party_auth/urls.py
@@ -0,0 +1,7 @@
+"""Url configuration for the auth module."""
+
+from django.conf.urls import include, patterns, url
+
+urlpatterns = patterns(
+    '', url(r'^auth/', include('social.apps.django_app.urls', namespace='social')),
+)
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 373704b5e26fa90b3f86e36b36581edf8e59077e..dc8a726d06807ab621916e2eb4b5d8897addfc28 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -385,3 +385,6 @@ TIME_ZONE_DISPLAYED_FOR_DEADLINES = ENV_TOKENS.get("TIME_ZONE_DISPLAYED_FOR_DEAD
 
 ##### X-Frame-Options response header settings #####
 X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS)
+
+##### Third-party auth options ################################################
+THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 197e677b5f4be99778eb196969c2f14e66a2cb64..35e5a39f68140974656ec2e702c563f911c68f54 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -234,6 +234,10 @@ FEATURES = {
 
     # Turn on/off Microsites feature
     'USE_MICROSITES': False,
+
+    # Turn on third-party auth. Disabled for now because full implementations are not yet available. Remember to syncdb
+    # if you enable this; we don't create tables by default.
+    'ENABLE_THIRD_PARTY_AUTH': False,
 }
 
 # Used for A/B testing
@@ -1247,6 +1251,7 @@ LINKEDIN_API = {
     'COMPANY_ID': '2746406',
 }
 
+
 ##### ACCOUNT LOCKOUT DEFAULT PARAMETERS #####
 MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = 5
 MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
@@ -1465,3 +1470,7 @@ for app_name in OPTIONAL_APPS:
         except ImportError:
             continue
     INSTALLED_APPS += (app_name,)
+
+# Stub for third_party_auth options.
+# See common/djangoapps/third_party_auth/settings.py for configuration details.
+THIRD_PARTY_AUTH = {}
diff --git a/lms/startup.py b/lms/startup.py
index 1c8cdc8ef74a6ef5610627e235691f635c9b191d..462fb3f8fd40a59a674337969a624f9b577a8098 100644
--- a/lms/startup.py
+++ b/lms/startup.py
@@ -26,6 +26,9 @@ def run():
     if settings.FEATURES.get('USE_MICROSITES', False):
         enable_microsites()
 
+    if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
+        enable_third_party_auth()
+
 
 def enable_theme():
     """
@@ -99,3 +102,14 @@ def enable_microsites():
         edxmako.startup.run()
 
         settings.STATICFILES_DIRS.insert(0, microsites_root)
+
+
+def enable_third_party_auth():
+    """
+    Enable the use of third_party_auth, which allows users to sign in to edX
+    using other identity providers. For configuration details, see
+    common/djangoapps/third_party_auth/settings.py.
+    """
+
+    from third_party_auth import settings as auth_settings
+    auth_settings.apply_settings(settings.THIRD_PARTY_AUTH, settings)
diff --git a/lms/urls.py b/lms/urls.py
index 4a45c1df97eaef433d31a4bf2c9d9d3a4deb121f..6823bc11bc121c94fd6f7c2ee26bbccd5d6f7037 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -488,6 +488,12 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
         url(r'^auto_auth$', 'student.views.auto_auth'),
     )
 
+# Third-party auth.
+if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
+    urlpatterns += (
+        url(r'', include('third_party_auth.urls')),
+    )
+
 urlpatterns = patterns(*urlpatterns)
 
 if settings.DEBUG:
diff --git a/pylintrc b/pylintrc
index aacfc999ff9548d62ab0096f106db365ef65eb53..21966c7f8ac188f3c3c74ece55c432c498e0776f 100644
--- a/pylintrc
+++ b/pylintrc
@@ -153,10 +153,10 @@ const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$
 class-rgx=[A-Z_][a-zA-Z0-9]+$
 
 # Regular expression which should only match correct function names
-function-rgx=[a-z_][a-z0-9_]{2,30}$
+function-rgx=([a-z_][a-z0-9_]{2,30}|test_[a-z0-9_]+)$
 
 # Regular expression which should only match correct method names
-method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff)$
+method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$
 
 # Regular expression which should only match correct instance attribute names
 attr-rgx=[a-z_][a-z0-9_]{2,30}$
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 115112032144c6466abb24864a2d0960a700a06e..d649bd6658a0fae883fa3dc302ec049d0d5ae7e4 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -60,6 +60,7 @@ pyparsing==1.5.6
 python-memcached==1.48
 python-openid==2.2.5
 python-dateutil==2.1
+python-social-auth==0.1.21
 pytz==2012h
 pysrt==0.4.7
 PyYAML==3.10