From f9b5a9173fb5fff396013f99215bd8e28b680453 Mon Sep 17 00:00:00 2001
From: Will Daly <will@edx.org>
Date: Sun, 26 Oct 2014 15:15:37 -0400
Subject: [PATCH] Redirect users to the track selection page or ?next page when
 using third party auth

Set marketing site logged in cookie from third party auth.
---
 common/djangoapps/course_modes/helpers.py     |   0
 common/djangoapps/course_modes/models.py      |  65 ++++++
 .../course_modes/tests/test_models.py         |  17 ++
 .../course_modes/tests/test_views.py          |  60 +++---
 common/djangoapps/course_modes/views.py       |  41 ++--
 common/djangoapps/student/helpers.py          | 113 +++++++++++
 .../student/tests/test_enrollment.py          |  43 ----
 .../tests/test_login_registration_forms.py    | 185 ++++++++++++++++++
 common/djangoapps/student/views.py            |  60 ++----
 .../djangoapps/third_party_auth/__init__.py   |  15 ++
 .../djangoapps/third_party_auth/pipeline.py   | 171 +++++++++++++---
 .../djangoapps/third_party_auth/settings.py   |   3 +-
 .../third_party_auth/tests/specs/base.py      |  47 +++++
 .../tests/test_change_enrollment.py           | 132 +++++++++++++
 .../third_party_auth/tests/testutil.py        |   2 +
 common/templates/course_modes/choose.html     |   8 +-
 .../courseware/features/certificates.py       |   2 +-
 lms/envs/test.py                              |  11 ++
 lms/templates/login.html                      |   2 +-
 lms/templates/register.html                   |   2 +-
 .../verify_student/_verification_header.html  |   4 +-
 21 files changed, 815 insertions(+), 168 deletions(-)
 create mode 100644 common/djangoapps/course_modes/helpers.py
 create mode 100644 common/djangoapps/student/helpers.py
 create mode 100644 common/djangoapps/student/tests/test_login_registration_forms.py
 create mode 100644 common/djangoapps/third_party_auth/tests/test_change_enrollment.py

diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py
index 94466bf20d0..b0bf62afa97 100644
--- a/common/djangoapps/course_modes/models.py
+++ b/common/djangoapps/course_modes/models.py
@@ -182,6 +182,71 @@ class CourseMode(models.Model):
                 return True
         return False
 
+    @classmethod
+    def can_auto_enroll(cls, course_id, modes_dict=None):
+        """Check whether students should be auto-enrolled in the course.
+
+        If a course is behind a paywall (e.g. professional ed or white-label),
+        then users should NOT be auto-enrolled.  Instead, the user will
+        be enrolled when he/she completes the payment flow.
+
+        Otherwise, users can be enrolled in the default mode "honor"
+        with the option to upgrade later.
+
+        Args:
+            course_id (CourseKey): The course to check.
+
+        Keyword Args:
+            modes_dict (dict): If provided, use these course modes.
+                Useful for avoiding unnecessary database queries.
+
+        Returns:
+            bool
+
+        """
+        if modes_dict is None:
+            modes_dict = cls.modes_for_course_dict(course_id)
+
+        # Professional mode courses are always behind a paywall
+        if "professional" in modes_dict:
+            return False
+
+        # White-label uses course mode honor with a price
+        # to indicate that the course is behind a paywall.
+        if cls.is_white_label(course_id, modes_dict=modes_dict):
+            return False
+
+        # Check that the default mode is available.
+        return ("honor" in modes_dict)
+
+    @classmethod
+    def is_white_label(cls, course_id, modes_dict=None):
+        """Check whether a course is a "white label" (paid) course.
+
+        By convention, white label courses have a course mode slug "honor"
+        and a price.
+
+        Args:
+            course_id (CourseKey): The course to check.
+
+        Keyword Args:
+            modes_dict (dict): If provided, use these course modes.
+                Useful for avoiding unnecessary database queries.
+
+        Returns:
+            bool
+
+        """
+        if modes_dict is None:
+            modes_dict = cls.modes_for_course_dict(course_id)
+
+        # White-label uses course mode honor with a price
+        # to indicate that the course is behind a paywall.
+        if "honor" in modes_dict and len(modes_dict) == 1:
+            if modes_dict["honor"].min_price > 0 or modes_dict["honor"].suggested_prices != '':
+                return True
+        return False
+
     @classmethod
     def min_course_price_for_currency(cls, course_id, currency):
         """
diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py
index 88571d449d7..85a87b3a724 100644
--- a/common/djangoapps/course_modes/tests/test_models.py
+++ b/common/djangoapps/course_modes/tests/test_models.py
@@ -7,12 +7,14 @@ Replace this with more appropriate tests for your application.
 
 from datetime import datetime, timedelta
 import pytz
+import ddt
 
 from opaque_keys.edx.locations import SlashSeparatedCourseKey
 from django.test import TestCase
 from course_modes.models import CourseMode, Mode
 
 
+@ddt.ddt
 class CourseModeModelTest(TestCase):
     """
     Tests for the CourseMode model
@@ -146,3 +148,18 @@ class CourseModeModelTest(TestCase):
         honor.suggested_prices = '5, 10, 15'
         honor.save()
         self.assertTrue(CourseMode.has_payment_options(self.course_key))
+
+    @ddt.data(
+        ([], True),
+        ([("honor", 0), ("audit", 0), ("verified", 100)], True),
+        ([("honor", 100)], False),
+        ([("professional", 100)], False),
+    )
+    @ddt.unpack
+    def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
+        # Create the modes and min prices
+        for mode_slug, min_price in modes_and_prices:
+            self.create_mode(mode_slug, mode_slug.capitalize(), min_price=min_price)
+
+        # Verify that we can or cannot auto enroll
+        self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_auto_enroll)
diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py
index 30b242671b2..ed6d817ad33 100644
--- a/common/djangoapps/course_modes/tests/test_views.py
+++ b/common/djangoapps/course_modes/tests/test_views.py
@@ -32,41 +32,33 @@ class CourseModeViewTest(ModuleStoreTestCase):
         self.client.login(username=self.user.username, password="edx")
 
     @ddt.data(
-        # is_active?, enrollment_mode, upgrade?, redirect?
-        (True, 'verified', True, False),   # User has an active verified enrollment and is trying to upgrade
-        (True, 'verified', False, True),   # User has an active verified enrollment and is not trying to upgrade
-        (True, 'honor', True, False),      # User has an active honor enrollment and is trying to upgrade
-        (True, 'honor', False, False),     # User has an active honor enrollment and is not trying to upgrade
-        (True, 'audit', True, False),      # User has an active audit enrollment and is trying to upgrade
-        (True, 'audit', False, False),     # User has an active audit enrollment and is not trying to upgrade
-        (False, 'verified', True, True),   # User has an inactive verified enrollment and is trying to upgrade
-        (False, 'verified', False, True),  # User has an inactive verified enrollment and is not trying to upgrade
-        (False, 'honor', True, True),      # User has an inactive honor enrollment and is trying to upgrade
-        (False, 'honor', False, True),     # User has an inactive honor enrollment and is not trying to upgrade
-        (False, 'audit', True, True),      # User has an inactive audit enrollment and is trying to upgrade
-        (False, 'audit', False, True),     # User has an inactive audit enrollment and is not trying to upgrade
+        # is_active?, enrollment_mode, redirect?
+        (True, 'verified', True),
+        (True, 'honor', False),
+        (True, 'audit', False),
+        (False, 'verified', False),
+        (False, 'honor', False),
+        (False, 'audit', False),
+        (False, None, False),
     )
     @ddt.unpack
-    def test_redirect_to_dashboard(self, is_active, enrollment_mode, upgrade, redirect):
+    def test_redirect_to_dashboard(self, is_active, enrollment_mode, redirect):
         # Create the course modes
         for mode in ('audit', 'honor', 'verified'):
             CourseModeFactory(mode_slug=mode, course_id=self.course.id)
 
         # Enroll the user in the test course
-        CourseEnrollmentFactory(
-            is_active=is_active,
-            mode=enrollment_mode,
-            course_id=self.course.id,
-            user=self.user
-        )
+        if enrollment_mode is not None:
+            CourseEnrollmentFactory(
+                is_active=is_active,
+                mode=enrollment_mode,
+                course_id=self.course.id,
+                user=self.user
+            )
 
         # Configure whether we're upgrading or not
-        get_params = {}
-        if upgrade:
-            get_params = {'upgrade': True}
-
         url = reverse('course_modes_choose', args=[unicode(self.course.id)])
-        response = self.client.get(url, get_params)
+        response = self.client.get(url)
 
         # Check whether we were correctly redirected
         if redirect:
@@ -74,7 +66,19 @@ class CourseModeViewTest(ModuleStoreTestCase):
         else:
             self.assertEquals(response.status_code, 200)
 
-    def test_redirect_to_dashboard_no_enrollment(self):
+    def test_upgrade_copy(self):
+        # Create the course modes
+        for mode in ('audit', 'honor', 'verified'):
+            CourseModeFactory(mode_slug=mode, course_id=self.course.id)
+
+        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+        response = self.client.get(url, {"upgrade": True})
+
+        # Verify that the upgrade copy is displayed instead
+        # of the usual text.
+        self.assertContains(response, "Upgrade Your Enrollment")
+
+    def test_no_enrollment(self):
         # Create the course modes
         for mode in ('audit', 'honor', 'verified'):
             CourseModeFactory(mode_slug=mode, course_id=self.course.id)
@@ -83,7 +87,7 @@ class CourseModeViewTest(ModuleStoreTestCase):
         url = reverse('course_modes_choose', args=[unicode(self.course.id)])
         response = self.client.get(url)
 
-        self.assertRedirects(response, reverse('dashboard'))
+        self.assertEquals(response.status_code, 200)
 
     @ddt.data(
         '',
@@ -121,7 +125,7 @@ class CourseModeViewTest(ModuleStoreTestCase):
         # TODO: Fix it so that response.templates works w/ mako templates, and then assert
         # that the right template rendered
 
-    def test_professional_registration(self):
+    def test_professional_enrollment(self):
         # The only course mode is professional ed
         CourseModeFactory(mode_slug='professional', course_id=self.course.id)
 
diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index b70e9e30283..d4147b6fa3e 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -17,6 +17,7 @@ from course_modes.models import CourseMode
 from courseware.access import has_access
 from student.models import CourseEnrollment
 from opaque_keys.edx.locations import SlashSeparatedCourseKey
+from opaque_keys.edx.keys import CourseKey
 from util.db import commit_on_success_with_read_committed
 from xmodule.modulestore.django import modulestore
 
@@ -26,10 +27,10 @@ class ChooseModeView(View):
 
     When a get request is used, shows the selection page.
 
-    When a post request is used, assumes that it is a form submission 
+    When a post request is used, assumes that it is a form submission
     from the selection page, parses the response, and then sends user
     to the next step in the flow.
-    
+
     """
 
     @method_decorator(login_required)
@@ -48,28 +49,19 @@ class ChooseModeView(View):
             Response
 
         """
-        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
-        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
-        
+        course_key = CourseKey.from_string(course_id)
+
         upgrade = request.GET.get('upgrade', False)
         request.session['attempting_upgrade'] = upgrade
 
-        # Students will already have an active course enrollment at this stage,
-        # but we should still show them the "choose your track" page so they have
-        # the option to enter the verification/payment flow.
-        go_to_dashboard = (
-            not upgrade and enrollment_mode in ['verified', 'professional']
-        )
-
-        if go_to_dashboard:
-            return redirect(reverse('dashboard'))
-
+        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
         modes = CourseMode.modes_for_course_dict(course_key)
 
         # We assume that, if 'professional' is one of the modes, it is the *only* mode.
         # If we offer more modes alongside 'professional' in the future, this will need to route
         # to the usual "choose your track" page.
-        if "professional" in modes:
+        has_enrolled_professional = (enrollment_mode == "professional" and is_active)
+        if "professional" in modes and not has_enrolled_professional:
             return redirect(
                 reverse(
                     'verify_student_show_requirements',
@@ -77,14 +69,15 @@ class ChooseModeView(View):
                 )
             )
 
-        # If a user's course enrollment is inactive at this stage, the track
-        # selection page may have been visited directly, so we should redirect
-        # the user to their dashboard. By the time the user gets here during the
-        # normal registration process, they will already have an activated enrollment;
-        # the button appearing on the track selection page only redirects the user to
-        # the dashboard, and we don't want the user to be confused when they click the
-        # honor button and are taken to their dashboard without being enrolled.
-        if not is_active:
+        # If there isn't a verified mode available, then there's nothing
+        # to do on this page.  The user has almost certainly been auto-registered
+        # in the "honor" track by this point, so we send the user
+        # to the dashboard.
+        if not CourseMode.has_verified_mode(modes):
+            return redirect(reverse('dashboard'))
+
+        # If a user has already paid, redirect them to the dashboard.
+        if is_active and enrollment_mode in CourseMode.VERIFIED_MODES:
             return redirect(reverse('dashboard'))
 
         donation_for_course = request.session.get("donation_for_course", {})
diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py
new file mode 100644
index 00000000000..23bba3d0dd1
--- /dev/null
+++ b/common/djangoapps/student/helpers.py
@@ -0,0 +1,113 @@
+"""Helpers for the student app. """
+import time
+from django.utils.http import cookie_date
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from opaque_keys.edx.keys import CourseKey
+from course_modes.models import CourseMode
+from third_party_auth import (  # pylint: disable=W0611
+    pipeline, provider,
+    is_enabled as third_party_auth_enabled
+)
+
+
+def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
+    """Retrieve URLs for each enabled third-party auth provider.
+
+    These URLs are used on the "sign up" and "sign in" buttons
+    on the login/registration forms to allow users to begin
+    authentication with a third-party provider.
+
+    Optionally, we can redirect the user to an arbitrary
+    url after auth completes successfully.  We use this
+    to redirect the user to a page that required login,
+    or to send users to the payment flow when enrolling
+    in a course.
+
+    Args:
+        auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER`
+
+    Keyword Args:
+        redirect_url (unicode): If provided, send users to this URL
+            after they successfully authenticate.
+
+        course_id (unicode): The ID of the course the user is enrolling in.
+            We use this to send users to the track selection page
+            if the course has a payment option.
+            Note that `redirect_url` takes precedence over the redirect
+            to the track selection page.
+
+    Returns:
+        dict mapping provider names to URLs
+
+    """
+    if not third_party_auth_enabled():
+        return {}
+
+    if redirect_url is not None:
+        pipeline_redirect = redirect_url
+    elif course_id is not None:
+        # If the course is white-label (paid), then we send users
+        # to the shopping cart.  (There is a third party auth pipeline
+        # step that will add the course to the cart.)
+        if CourseMode.is_white_label(CourseKey.from_string(course_id)):
+            pipeline_redirect = reverse("shoppingcart.views.show_cart")
+
+        # Otherwise, send the user to the track selection page.
+        # The track selection page may redirect the user to the dashboard
+        # (if the only available mode is honor), or directly to verification
+        # (for professional ed).
+        else:
+            pipeline_redirect = reverse(
+                "course_modes_choose",
+                kwargs={'course_id': unicode(course_id)}
+            )
+    else:
+        pipeline_redirect = None
+
+    return {
+        provider.NAME: pipeline.get_login_url(
+            provider.NAME, auth_entry,
+            enroll_course_id=course_id,
+            redirect_url=pipeline_redirect
+        )
+        for provider in provider.Registry.enabled()
+    }
+
+
+def set_logged_in_cookie(request, response):
+    """Set a cookie indicating that the user is logged in.
+
+    Some installations have an external marketing site configured
+    that displays a different UI when the user is logged in
+    (e.g. a link to the student dashboard instead of to the login page)
+
+    Arguments:
+        request (HttpRequest): The request to the view, used to calculate
+            the cookie's expiration date based on the session expiration date.
+        response (HttpResponse): The response on which the cookie will be set.
+
+    Returns:
+        HttpResponse
+
+    """
+    if request.session.get_expire_at_browser_close():
+        max_age = None
+        expires = None
+    else:
+        max_age = request.session.get_expiry_age()
+        expires_time = time.time() + max_age
+        expires = cookie_date(expires_time)
+
+    response.set_cookie(
+        settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age,
+        expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
+        path='/', secure=None, httponly=None,
+    )
+
+    return response
+
+
+def is_logged_in_cookie_set(request):
+    """Check whether the request has the logged in cookie set. """
+    return settings.EDXMKTG_COOKIE_NAME in request.COOKIES
diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py
index 9565fc5a234..920803d34ca 100644
--- a/common/djangoapps/student/tests/test_enrollment.py
+++ b/common/djangoapps/student/tests/test_enrollment.py
@@ -11,12 +11,8 @@ from xmodule.modulestore.tests.django_utils import (
     ModuleStoreTestCase, mixed_store_config
 )
 from xmodule.modulestore.tests.factories import CourseFactory
-from social.strategies.django_strategy import DjangoStrategy
-from django.test.client import RequestFactory
 from student.tests.factories import UserFactory, CourseModeFactory
 from student.models import CourseEnrollment
-from student.views import register_user
-from third_party_auth.pipeline import change_enrollment as change_enrollment_third_party
 
 # Since we don't need any XML course fixtures, use a modulestore configuration
 # that disables the XML modulestore.
@@ -97,45 +93,6 @@ class EnrollmentTest(ModuleStoreTestCase):
             self.assertTrue(is_active)
             self.assertEqual(course_mode, enrollment_mode)
 
-    def test_enroll_from_third_party_redirect(self):
-        """
-        Test that, when a user visits the registration page *after* visiting a course,
-        if they go on to register and/or log in via third-party auth, they'll be enrolled
-        in that course.
-
-        The testing here is a bit hackish, since we just ping the registration page, then
-        directly call the step in the third party pipeline that registers the user if
-        `registration_course_id` is set in the session, but it should catch any major breaks.
-        """
-        self.client.logout()
-        self.client.get(reverse('register_user'), {'course_id': self.course.id})
-        self.client.login(username=self.USERNAME, password=self.PASSWORD)
-        dummy_request = RequestFactory().request()
-        dummy_request.session = self.client.session
-        strategy = DjangoStrategy(RequestFactory, request=dummy_request)
-        change_enrollment_third_party(is_register=True, strategy=strategy, user=self.user)
-        self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
-
-    def test_no_prof_ed_third_party_autoenroll(self):
-        """
-        Test that a user authenticating via third party auth while attempting to enroll
-        in a professional education course is not automatically enrolled in the course.
-        """
-        self.client.logout()
-
-        # Create the course mode required for this test case
-        CourseModeFactory(course_id=self.course.id, mode_slug='professional')
-
-        self.client.get(reverse('register_user'), {'course_id': self.course.id})
-        self.client.login(username=self.USERNAME, password=self.PASSWORD)
-        dummy_request = RequestFactory().request()
-        dummy_request.session = self.client.session
-        strategy = DjangoStrategy(RequestFactory, request=dummy_request)
-        change_enrollment_third_party(is_register=True, strategy=strategy, user=self.user)
-
-        # Verify that the user has not been enrolled in the course
-        self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
-
     def test_unenroll(self):
         # Enroll the student in the course
         CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
diff --git a/common/djangoapps/student/tests/test_login_registration_forms.py b/common/djangoapps/student/tests/test_login_registration_forms.py
new file mode 100644
index 00000000000..598c9200210
--- /dev/null
+++ b/common/djangoapps/student/tests/test_login_registration_forms.py
@@ -0,0 +1,185 @@
+"""Tests for the login and registration form rendering. """
+import urllib
+import unittest
+from mock import patch
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+import ddt
+from django.test.utils import override_settings
+from xmodule.modulestore.tests.factories import CourseFactory
+from student.tests.factories import CourseModeFactory
+from xmodule.modulestore.tests.django_utils import (
+    ModuleStoreTestCase, mixed_store_config
+)
+
+
+# This relies on third party auth being enabled and configured
+# in the test settings.  See the setting `THIRD_PARTY_AUTH`
+# and the feature flag `ENABLE_THIRD_PARTY_AUTH`
+THIRD_PARTY_AUTH_BACKENDS = ["google-oauth2", "facebook"]
+THIRD_PARTY_AUTH_PROVIDERS = ["Google", "Facebook"]
+
+# Since we don't need any XML course fixtures, use a modulestore configuration
+# that disables the XML modulestore.
+MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
+
+
+def _third_party_login_url(backend_name, auth_entry, course_id=None, redirect_url=None):
+    """Construct the login URL to start third party authentication. """
+    params = [("auth_entry", auth_entry)]
+    if redirect_url:
+        params.append(("next", redirect_url))
+    if course_id:
+        params.append(("enroll_course_id", course_id))
+
+    return u"{url}?{params}".format(
+        url=reverse("social:begin", kwargs={"backend": backend_name}),
+        params=urllib.urlencode(params)
+    )
+
+
+@ddt.ddt
+@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class LoginFormTest(ModuleStoreTestCase):
+    """Test rendering of the login form. """
+
+    def setUp(self):
+        self.url = reverse("signin_user")
+        self.course = CourseFactory.create()
+        self.course_id = unicode(self.course.id)
+        self.course_modes_url = reverse("course_modes_choose", kwargs={"course_id": self.course_id})
+        self.courseware_url = reverse("courseware", args=[self.course_id])
+
+    @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
+    @ddt.data(THIRD_PARTY_AUTH_PROVIDERS)
+    def test_third_party_auth_disabled(self, provider_name):
+        response = self.client.get(self.url)
+        self.assertNotContains(response, provider_name)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_third_party_auth_no_course_id(self, backend_name):
+        response = self.client.get(self.url)
+        expected_url = _third_party_login_url(backend_name, "login")
+        self.assertContains(response, expected_url)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_third_party_auth_with_course_id(self, backend_name):
+        # Provide a course ID to the login page, simulating what happens
+        # when a user tries to enroll in a course without being logged in
+        response = self.client.get(self.url, {"course_id": self.course_id})
+
+        # Expect that the course ID is added to the third party auth entry
+        # point, so that the pipeline will enroll the student and
+        # redirect the student to the track selection page.
+        expected_url = _third_party_login_url(
+            backend_name,
+            "login",
+            course_id=self.course_id,
+            redirect_url=self.course_modes_url
+        )
+        self.assertContains(response, expected_url)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_third_party_auth_with_white_label_course(self, backend_name):
+        # Set the course mode to honor with a min price,
+        # indicating that the course is behind a paywall.
+        CourseModeFactory.create(
+            course_id=self.course.id,
+            mode_slug="honor",
+            mode_display_name="Honor",
+            min_price=100
+        )
+
+        # Expect that we're redirected to the shopping cart
+        # instead of to the track selection page.
+        response = self.client.get(self.url, {"course_id": self.course_id})
+        expected_url = _third_party_login_url(
+            backend_name,
+            "login",
+            course_id=self.course_id,
+            redirect_url=reverse("shoppingcart.views.show_cart")
+        )
+        self.assertContains(response, expected_url)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_third_party_auth_with_redirect_url(self, backend_name):
+        # Try to access courseware while logged out, expecting to be
+        # redirected to the login page.
+        response = self.client.get(self.courseware_url, follow=True)
+        self.assertRedirects(
+            response,
+            u"{url}?next={redirect_url}".format(
+                url=reverse("accounts_login"),
+                redirect_url=self.courseware_url
+            )
+        )
+
+        # Verify that the third party auth URLs include the redirect URL
+        # The third party auth pipeline will redirect to this page
+        # once the user successfully authenticates.
+        expected_url = _third_party_login_url(
+            backend_name,
+            "login",
+            redirect_url=self.courseware_url
+        )
+        self.assertContains(response, expected_url)
+
+
+@ddt.ddt
+@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class RegisterFormTest(TestCase):
+    """Test rendering of the registration form. """
+
+    def setUp(self):
+        self.url = reverse("register_user")
+        self.course = CourseFactory.create()
+        self.course_id = unicode(self.course.id)
+        self.course_modes_url = reverse("course_modes_choose", kwargs={"course_id": self.course_id})
+
+    @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
+    @ddt.data(*THIRD_PARTY_AUTH_PROVIDERS)
+    def test_third_party_auth_disabled(self, provider_name):
+        response = self.client.get(self.url)
+        self.assertNotContains(response, provider_name)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_register_third_party_auth_no_course_id(self, backend_name):
+        response = self.client.get(self.url)
+        expected_url = _third_party_login_url(backend_name, "register")
+        self.assertContains(response, expected_url)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_register_third_party_auth_with_course_id(self, backend_name):
+        response = self.client.get(self.url, {"course_id": self.course_id})
+        expected_url = _third_party_login_url(
+            backend_name,
+            "register",
+            course_id=self.course_id,
+            redirect_url=self.course_modes_url
+        )
+        self.assertContains(response, expected_url)
+
+    @ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
+    def test_third_party_auth_with_white_label_course(self, backend_name):
+        # Set the course mode to honor with a min price,
+        # indicating that the course is behind a paywall.
+        CourseModeFactory.create(
+            course_id=self.course.id,
+            mode_slug="honor",
+            mode_display_name="Honor",
+            min_price=100
+        )
+
+        # Expect that we're redirected to the shopping cart
+        # instead of to the track selection page.
+        response = self.client.get(self.url, {"course_id": self.course_id})
+        expected_url = _third_party_login_url(
+            backend_name,
+            "register",
+            course_id=self.course_id,
+            redirect_url=reverse("shoppingcart.views.show_cart")
+        )
+        self.assertContains(response, expected_url)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index eb23c1f8765..ab33ba9fea2 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -93,6 +93,7 @@ from util.password_policy_validators import (
 )
 
 from third_party_auth import pipeline, provider
+from student.helpers import auth_pipeline_urls, set_logged_in_cookie
 from xmodule.error_module import ErrorDescriptor
 from shoppingcart.models import CourseRegistrationCode
 
@@ -352,13 +353,15 @@ def signin_user(request):
     if request.user.is_authenticated():
         return redirect(reverse('dashboard'))
 
+    course_id = request.GET.get('course_id')
     context = {
-        'course_id': request.GET.get('course_id'),
+        'course_id': course_id,
         'enrollment_action': request.GET.get('enrollment_action'),
         # Bool injected into JS to submit form if we're inside a running third-
         # party auth pipeline; distinct from the actual instance of the running
         # pipeline, if any.
         'pipeline_running': 'true' if pipeline.running(request) else 'false',
+        'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, course_id=course_id),
         'platform_name': microsite.get_value(
             'platform_name',
             settings.PLATFORM_NAME
@@ -380,12 +383,15 @@ def register_user(request, extra_context=None):
         # and registration is disabled.
         return external_auth.views.redirect_with_get('root', request.GET)
 
+    course_id = request.GET.get('course_id')
+
     context = {
-        'course_id': request.GET.get('course_id'),
+        'course_id': course_id,
         'email': '',
         'enrollment_action': request.GET.get('enrollment_action'),
         'name': '',
         'running_pipeline': None,
+        'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, course_id=course_id),
         'platform_name': microsite.get_value(
             'platform_name',
             settings.PLATFORM_NAME
@@ -394,10 +400,6 @@ def register_user(request, extra_context=None):
         'username': '',
     }
 
-    # We save this so, later on, we can determine what course motivated a user's signup
-    # if they actually complete the registration process
-    request.session['registration_course_id'] = context['course_id']
-
     if extra_context is not None:
         context.update(extra_context)
 
@@ -798,14 +800,9 @@ def change_enrollment(request, check_access=True):
 
         available_modes = CourseMode.modes_for_course_dict(course_id)
 
-        # Handle professional ed as a special case.
-        # If professional ed is included in the list of available modes,
-        # then do NOT automatically enroll the student (we want them to pay first!)
-        # By convention, professional ed should be the *only* available course mode,
-        # if it's included at all -- anything else is a misconfiguration.  But if someone
-        # messes up and adds an additional course mode, we err on the side of NOT
-        # accidentally giving away free courses.
-        if "professional" not in available_modes:
+        # Check that auto enrollment is allowed for this course
+        # (= the course is NOT behind a paywall)
+        if CourseMode.can_auto_enroll(course_id):
             # Enroll the user using the default mode (honor)
             # We're assuming that users of the course enrollment table
             # will NOT try to look up the course enrollment model
@@ -821,7 +818,7 @@ def change_enrollment(request, check_access=True):
         # then send the user to the choose your track page.
         # (In the case of professional ed, this will redirect to a page that
         # funnels users directly into the verification / payment flow)
-        if len(available_modes) > 1 or "professional" in available_modes:
+        if CourseMode.has_verified_mode(available_modes):
             return HttpResponse(
                 reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)})
             )
@@ -902,6 +899,7 @@ def accounts_login(request):
 
     context = {
         'pipeline_running': 'false',
+        'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to),
         'platform_name': settings.PLATFORM_NAME,
     }
     return render_to_response('login.html', context)
@@ -1053,14 +1051,12 @@ def login_user(request, error=""):  # pylint: disable-msg=too-many-statements,un
             'username': username,
         })
 
-        # If the user entered the flow via a specific course page, we track that
-        registration_course_id = request.session.get('registration_course_id')
         analytics.track(
             user.id,
             "edx.bi.user.account.authenticated",
             {
                 'category': "conversion",
-                'label': registration_course_id,
+                'label': request.POST.get('course_id'),
                 'provider': None
             },
             context={
@@ -1069,7 +1065,6 @@ def login_user(request, error=""):  # pylint: disable-msg=too-many-statements,un
                 }
             }
         )
-        request.session['registration_course_id'] = None
 
     if user is not None and user.is_active:
         try:
@@ -1097,25 +1092,9 @@ def login_user(request, error=""):  # pylint: disable-msg=too-many-statements,un
             "redirect_url": redirect_url,
         })
 
-        # set the login cookie for the edx marketing site
-        # we want this cookie to be accessed via javascript
-        # so httponly is set to None
-
-        if request.session.get_expire_at_browser_close():
-            max_age = None
-            expires = None
-        else:
-            max_age = request.session.get_expiry_age()
-            expires_time = time.time() + max_age
-            expires = cookie_date(expires_time)
-
-        response.set_cookie(
-            settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age,
-            expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
-            path='/', secure=None, httponly=None,
-        )
-
-        return response
+        # Ensure that the external marketing site can
+        # detect that the user is logged in.
+        return set_logged_in_cookie(request, response)
 
     if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
         AUDIT_LOG.warning(u"Login failed - Account not active for user.id: {0}, resending activation".format(user.id))
@@ -1130,6 +1109,7 @@ def login_user(request, error=""):  # pylint: disable-msg=too-many-statements,un
     })  # TODO: this should be status code 400  # pylint: disable=fixme
 
 
+
 @ensure_csrf_cookie
 def logout_user(request):
     """
@@ -1536,13 +1516,12 @@ def create_account(request, post_override=None):  # pylint: disable-msg=too-many
             current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
             provider_name = current_provider.NAME
 
-        registration_course_id = request.session.get('registration_course_id')
         analytics.track(
             user.id,
             "edx.bi.user.account.registered",
             {
                 'category': 'conversion',
-                'label': registration_course_id,
+                'label': request.POST.get('course_id'),
                 'provider': provider_name
             },
             context={
@@ -1551,7 +1530,6 @@ def create_account(request, post_override=None):  # pylint: disable-msg=too-many
                 }
             }
         )
-        request.session['registration_course_id'] = None
 
     create_comments_service_user(user)
 
diff --git a/common/djangoapps/third_party_auth/__init__.py b/common/djangoapps/third_party_auth/__init__.py
index e69de29bb2d..c30f0076fab 100644
--- a/common/djangoapps/third_party_auth/__init__.py
+++ b/common/djangoapps/third_party_auth/__init__.py
@@ -0,0 +1,15 @@
+"""Third party authentication. """
+
+from microsite_configuration import microsite
+
+
+def is_enabled():
+    """Check whether third party authentication has been enabled. """
+
+    # We do this import internally to avoid initializing settings prematurely
+    from django.conf import settings
+
+    return microsite.get_value(
+        "ENABLE_THIRD_PARTY_AUTH",
+        settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH")
+    )
diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py
index a7ce75aae55..c4730d6dc3e 100644
--- a/common/djangoapps/third_party_auth/pipeline.py
+++ b/common/djangoapps/third_party_auth/pipeline.py
@@ -59,6 +59,8 @@ See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
 
 import random
 import string  # pylint: disable-msg=deprecated-module
+from collections import OrderedDict
+import urllib
 import analytics
 from eventtracking import tracker
 
@@ -69,7 +71,15 @@ from social.apps.django_app.default import models
 from social.exceptions import AuthException
 from social.pipeline import partial
 
-from student.models import CourseMode, CourseEnrollment, CourseEnrollmentException
+import student
+from shoppingcart.models import Order, PaidCourseRegistration  # pylint: disable=F0401
+from shoppingcart.exceptions import (  # pylint: disable=F0401
+    CourseDoesNotExistException,
+    ItemAlreadyInCartException,
+    AlreadyEnrolledInCourseException
+)
+from student.models import CourseEnrollment, CourseEnrollmentException
+from course_modes.models import CourseMode
 from opaque_keys.edx.keys import CourseKey
 
 from logging import getLogger
@@ -77,7 +87,24 @@ from logging import getLogger
 from . import provider
 
 
+# These are the query string params you can pass
+# to the URL that starts the authentication process.
+#
+# `AUTH_ENTRY_KEY` is required and indicates how the user
+# enters the authentication process.
+#
+# `AUTH_REDIRECT_KEY` provides an optional URL to redirect
+# to upon successful authentication
+# (if not provided, defaults to `_SOCIAL_AUTH_LOGIN_REDIRECT_URL`)
+#
+# `AUTH_ENROLL_COURSE_ID_KEY` provides the course ID that a student
+# is trying to enroll in, used to generate analytics events
+# and auto-enroll students.
+
 AUTH_ENTRY_KEY = 'auth_entry'
+AUTH_REDIRECT_KEY = 'next'
+AUTH_ENROLL_COURSE_ID_KEY = 'enroll_course_id'
+
 AUTH_ENTRY_DASHBOARD = 'dashboard'
 AUTH_ENTRY_LOGIN = 'login'
 AUTH_ENTRY_PROFILE = 'profile'
@@ -177,15 +204,25 @@ def _get_enabled_provider_by_name(provider_name):
     return enabled_provider
 
 
-def _get_url(view_name, backend_name, auth_entry=None):
+def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None):
     """Creates a URL to hook into social auth endpoints."""
     kwargs = {'backend': backend_name}
     url = reverse(view_name, kwargs=kwargs)
 
+    query_params = OrderedDict()
     if auth_entry:
-        url += '?%s=%s' % (AUTH_ENTRY_KEY, auth_entry)
+        query_params[AUTH_ENTRY_KEY] = auth_entry
+
+    if redirect_url:
+        query_params[AUTH_REDIRECT_KEY] = redirect_url
+
+    if enroll_course_id:
+        query_params[AUTH_ENROLL_COURSE_ID_KEY] = enroll_course_id
 
-    return url
+    return u"{url}?{params}".format(
+        url=url,
+        params=urllib.urlencode(query_params)
+    )
 
 
 def get_complete_url(backend_name):
@@ -226,7 +263,7 @@ def get_disconnect_url(provider_name):
     return _get_url('social:disconnect', enabled_provider.BACKEND_CLASS.name)
 
 
-def get_login_url(provider_name, auth_entry):
+def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None):
     """Gets the login URL for the endpoint that kicks off auth with a provider.
 
     Args:
@@ -236,6 +273,13 @@ def get_login_url(provider_name, auth_entry):
             for the auth pipeline. Used by the pipeline for later branching.
             Must be one of _AUTH_ENTRY_CHOICES.
 
+    Keyword Args:
+        redirect_url (string): If provided, redirect to this URL at the end
+            of the authentication process.
+
+        enroll_course_id (string): If provided, auto-enroll the user in this
+            course upon successful authentication.
+
     Returns:
         String. URL that starts the auth pipeline for a provider.
 
@@ -244,7 +288,13 @@ def get_login_url(provider_name, auth_entry):
     """
     assert auth_entry in _AUTH_ENTRY_CHOICES
     enabled_provider = _get_enabled_provider_by_name(provider_name)
-    return _get_url('social:begin', enabled_provider.BACKEND_CLASS.name, auth_entry=auth_entry)
+    return _get_url(
+        'social:begin',
+        enabled_provider.BACKEND_CLASS.name,
+        auth_entry=auth_entry,
+        redirect_url=redirect_url,
+        enroll_course_id=enroll_course_id
+    )
 
 
 def get_duplicate_provider(messages):
@@ -378,8 +428,54 @@ def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboar
     if is_register and user_unset:
         return redirect('/register', name='register_user')
 
+
 @partial.partial
-def login_analytics(*args, **kwargs):
+def set_logged_in_cookie(backend=None, user=None, request=None, *args, **kwargs):
+    """This pipeline step sets the "logged in" cookie for authenticated users.
+
+    Some installations have a marketing site front-end separate from
+    edx-platform.  Those installations sometimes display different
+    information for logged in versus anonymous users (e.g. a link
+    to the student dashboard instead of the login page.)
+
+    Since social auth uses Django's native `login()` method, it bypasses
+    our usual login view that sets this cookie.  For this reason, we need
+    to set the cookie ourselves within the pipeline.
+
+    The procedure for doing this is a little strange.  On the one hand,
+    we need to send a response to the user in order to set the cookie.
+    On the other hand, we don't want to drop the user out of the pipeline.
+
+    For this reason, we send a redirect back to the "complete" URL,
+    so users immediately re-enter the pipeline.  The redirect response
+    contains a header that sets the logged in cookie.
+
+    If the user is not logged in, or the logged in cookie is already set,
+    the function returns `None`, indicating that control should pass
+    to the next pipeline step.
+
+    """
+    if user is not None and user.is_authenticated():
+        if request is not None:
+            # Check that the cookie isn't already set.
+            # This ensures that we allow the user to continue to the next
+            # pipeline step once he/she has the cookie set by this step.
+            has_cookie = student.helpers.is_logged_in_cookie_set(request)
+            if not has_cookie:
+                try:
+                    redirect_url = get_complete_url(backend.name)
+                except ValueError:
+                    # If for some reason we can't get the URL, just skip this step
+                    # This may be overly paranoid, but it's far more important that
+                    # the user log in successfully than that the cookie is set.
+                    pass
+                else:
+                    response = redirect(redirect_url)
+                    return student.helpers.set_logged_in_cookie(request, response)
+
+
+@partial.partial
+def login_analytics(strategy, *args, **kwargs):
     """ Sends login info to Segment.io """
     event_name = None
 
@@ -396,14 +492,13 @@ def login_analytics(*args, **kwargs):
             event_name = action_to_event_name[action]
 
     if event_name is not None:
-        registration_course_id = kwargs['request'].session.get('registration_course_id')
         tracking_context = tracker.get_tracker().resolve_context()
         analytics.track(
             kwargs['user'].id,
             event_name,
             {
                 'category': "conversion",
-                'label': registration_course_id,
+                'label': strategy.session_get('enroll_course_id'),
                 'provider': getattr(kwargs['backend'], 'name')
             },
             context={
@@ -413,22 +508,54 @@ def login_analytics(*args, **kwargs):
             }
         )
 
-#@partial.partial
-def change_enrollment(*args, **kwargs):
-    """
-    If the user accessed the third party auth flow after trying to register for
-    a course, we automatically log them into that course.
-    """
-    if kwargs['strategy'].session_get('registration_course_id'):
-        course_id = CourseKey.from_string(
-            kwargs['strategy'].session_get('registration_course_id')
-        )
-        available_modes = CourseMode.modes_for_course_dict(course_id)
 
-        if 'honor' in available_modes:
+@partial.partial
+def change_enrollment(strategy, user=None, *args, **kwargs):
+    """Enroll a user in a course.
+
+    If a user entered the authentication flow when trying to enroll
+    in a course, then attempt to enroll the user.
+    We will try to do this if the pipeline was started with the
+    querystring param `enroll_course_id`.
+
+    In the following cases, we can't enroll the user:
+        * The course does not have an honor mode.
+        * The course has an honor mode with a minimum price.
+        * The course is not yet open for enrollment.
+        * The course does not exist.
+
+    If we can't enroll the user now, then skip this step.
+    For paid courses, users will be redirected to the payment flow
+    upon completion of the authentication pipeline
+    (configured using the ?next parameter to the third party auth login url).
+
+    """
+    enroll_course_id = strategy.session_get('enroll_course_id')
+    if enroll_course_id:
+        course_id = CourseKey.from_string(enroll_course_id)
+        modes = CourseMode.modes_for_course_dict(course_id)
+        if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
             try:
-                CourseEnrollment.enroll(kwargs['user'], course_id)
+                CourseEnrollment.enroll(user, course_id, check_access=True)
             except CourseEnrollmentException:
                 pass
             except Exception as ex:
                 logger.exception(ex)
+
+        # Handle white-label courses as a special case
+        # If a course is white-label, we should add it to the shopping cart.
+        elif CourseMode.is_white_label(course_id, modes_dict=modes):
+            try:
+                cart = Order.get_cart_for_user(user)
+                PaidCourseRegistration.add_to_order(cart, course_id)
+            except (
+                CourseDoesNotExistException,
+                ItemAlreadyInCartException,
+                AlreadyEnrolledInCourseException
+            ):
+                pass
+            # It's more important to complete login than to
+            # ensure that the course was added to the shopping cart.
+            # Log errors, but don't stop the authentication pipeline.
+            except Exception as ex:
+                logger.exception(ex)
diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py
index ecb430581f8..421d335b2c2 100644
--- a/common/djangoapps/third_party_auth/settings.py
+++ b/common/djangoapps/third_party_auth/settings.py
@@ -46,7 +46,7 @@ If true, it:
 from . import provider
 
 
-_FIELDS_STORED_IN_SESSION = ['auth_entry']
+_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id']
 _MIDDLEWARE_CLASSES = (
     'third_party_auth.middleware.ExceptionMiddleware',
 )
@@ -116,6 +116,7 @@ def _set_global_settings(django_settings):
         'social.pipeline.social_auth.associate_user',
         'social.pipeline.social_auth.load_extra_data',
         'social.pipeline.user.user_details',
+        'third_party_auth.pipeline.set_logged_in_cookie',
         'third_party_auth.pipeline.login_analytics',
         'third_party_auth.pipeline.change_enrollment',
     )
diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py
index 6d5ec1e3533..071700376d9 100644
--- a/common/djangoapps/third_party_auth/tests/specs/base.py
+++ b/common/djangoapps/third_party_auth/tests/specs/base.py
@@ -394,6 +394,19 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
         """Gets a user by email, using the given strategy."""
         return strategy.storage.user.user_model().objects.get(email=email)
 
+    def assert_logged_in_cookie_redirect(self, response):
+        """Verify that the user was redirected in order to set the logged in cookie. """
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(
+            response["Location"],
+            pipeline.get_complete_url(self.PROVIDER_CLASS.BACKEND_CLASS.name)
+        )
+        self.assertEqual(response.cookies[django_settings.EDXMKTG_COOKIE_NAME].value, 'true')
+
+    def set_logged_in_cookie(self, request):
+        """Simulate setting the marketing site cookie on the request. """
+        request.COOKIES[django_settings.EDXMKTG_COOKIE_NAME] = 'true'
+
     # Actual tests, executed once per child.
 
     def test_canceling_authentication_redirects_to_login_when_auth_entry_login(self):
@@ -430,6 +443,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
         self.assert_dashboard_response_looks_correct(student_views.dashboard(request), request.user, linked=False)
         self.assert_social_auth_does_not_exist_for_user(request.user, strategy)
 
+        # We should be redirected back to the complete page, setting
+        # the "logged in" cookie for the marketing site.
+        self.assert_logged_in_cookie_redirect(actions.do_complete(
+            request.social_strategy, social_views._do_login, request.user, None,  # pylint: disable-msg=protected-access
+            redirect_field_name=auth.REDIRECT_FIELD_NAME
+        ))
+
+        # Set the cookie and try again
+        self.set_logged_in_cookie(request)
+
         # Fire off the auth pipeline to link.
         self.assert_redirect_to_dashboard_looks_correct(actions.do_complete(
             request.social_strategy, social_views._do_login, request.user, None,  # pylint: disable-msg=protected-access
@@ -449,6 +472,9 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
             strategy, 'user@example.com', 'password', self.get_username())
         self.assert_social_auth_exists_for_user(user, strategy)
 
+        # We're already logged in, so simulate that the cookie is set correctly
+        self.set_logged_in_cookie(request)
+
         # Instrument the pipeline to get to the dashboard with the full
         # expected state.
         self.client.get(
@@ -561,6 +587,17 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
         # redirects to /auth/complete. In the browser ajax handlers will
         # redirect the user to the dashboard; we invoke it manually here.
         self.assert_json_success_response_looks_correct(student_views.login_user(strategy.request))
+
+        # We should be redirected back to the complete page, setting
+        # the "logged in" cookie for the marketing site.
+        self.assert_logged_in_cookie_redirect(actions.do_complete(
+            request.social_strategy, social_views._do_login, request.user, None,  # pylint: disable-msg=protected-access
+            redirect_field_name=auth.REDIRECT_FIELD_NAME
+        ))
+
+        # Set the cookie and try again
+        self.set_logged_in_cookie(request)
+
         self.assert_redirect_to_dashboard_looks_correct(
             actions.do_complete(strategy, social_views._do_login, user=user))
         self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user)
@@ -652,6 +689,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
         # social auth.
         self.assert_social_auth_does_not_exist_for_user(created_user, strategy)
 
+        # We should be redirected back to the complete page, setting
+        # the "logged in" cookie for the marketing site.
+        self.assert_logged_in_cookie_redirect(actions.do_complete(
+            request.social_strategy, social_views._do_login, request.user, None,  # pylint: disable-msg=protected-access
+            redirect_field_name=auth.REDIRECT_FIELD_NAME
+        ))
+
+        # Set the cookie and try again
+        self.set_logged_in_cookie(request)
+
         # Pick the pipeline back up. This will create the account association
         # and send the user to the dashboard, where the association will be
         # displayed.
diff --git a/common/djangoapps/third_party_auth/tests/test_change_enrollment.py b/common/djangoapps/third_party_auth/tests/test_change_enrollment.py
new file mode 100644
index 00000000000..73ecd1022b4
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/test_change_enrollment.py
@@ -0,0 +1,132 @@
+"""Tests for the change enrollment step of the pipeline. """
+
+import datetime
+import unittest
+import ddt
+import pytz
+from third_party_auth import pipeline
+from shoppingcart.models import Order, PaidCourseRegistration  # pylint: disable=F0401
+from social.apps.django_app import utils as social_utils
+from django.conf import settings
+from django.contrib.sessions.backends import cache
+from django.test import RequestFactory
+from django.test.utils import override_settings
+from xmodule.modulestore.tests.factories import CourseFactory
+from student.tests.factories import UserFactory, CourseModeFactory
+from student.models import CourseEnrollment
+from xmodule.modulestore.tests.django_utils import (
+    ModuleStoreTestCase, mixed_store_config
+)
+
+
+MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
+
+THIRD_PARTY_AUTH_CONFIGURED = (
+    settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and
+    getattr(settings, 'THIRD_PARTY_AUTH', {})
+)
+
+
+@unittest.skipUnless(THIRD_PARTY_AUTH_CONFIGURED, "Third party auth must be configured")
+@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@ddt.ddt
+class PipelineEnrollmentTest(ModuleStoreTestCase):
+    """Test that the pipeline auto-enrolls students upon successful authentication. """
+
+    BACKEND_NAME = "google-oauth2"
+
+    def setUp(self):
+        """Create a test course and user. """
+        super(PipelineEnrollmentTest, self).setUp()
+        self.course = CourseFactory.create()
+        self.user = UserFactory.create()
+
+    @ddt.data(
+        ([], "honor"),
+        (["honor", "verified", "audit"], "honor"),
+        (["professional"], None)
+    )
+    @ddt.unpack
+    def test_auto_enroll_step(self, course_modes, enrollment_mode):
+        # Create the course modes for the test case
+        for mode_slug in course_modes:
+            CourseModeFactory.create(
+                course_id=self.course.id,
+                mode_slug=mode_slug,
+                mode_display_name=mode_slug.capitalize()
+            )
+
+        # Simulate the pipeline step, passing in a course ID
+        # to indicate that the user was trying to enroll
+        # when they started the auth process.
+        strategy = self._fake_strategy()
+        strategy.session_set('enroll_course_id', unicode(self.course.id))
+
+        result = pipeline.change_enrollment(strategy, 1, user=self.user)  # pylint: disable=E1111,E1124
+        self.assertEqual(result, {})
+
+        # Check that the user was or was not enrolled
+        # (this will vary based on the course mode)
+        if enrollment_mode is not None:
+            actual_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
+            self.assertTrue(is_active)
+            self.assertEqual(actual_mode, enrollment_mode)
+        else:
+            self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
+
+    def test_add_white_label_to_cart(self):
+        # Create a white label course (honor with a minimum price)
+        CourseModeFactory.create(
+            course_id=self.course.id,
+            mode_slug="honor",
+            mode_display_name="Honor",
+            min_price=100
+        )
+
+        # Simulate the pipeline step for enrolling in this course
+        strategy = self._fake_strategy()
+        strategy.session_set('enroll_course_id', unicode(self.course.id))
+        result = pipeline.change_enrollment(strategy, 1, user=self.user)  # pylint: disable=E1111,E1124
+        self.assertEqual(result, {})
+
+        # Expect that the uesr is NOT enrolled in the course
+        # because the user has not yet paid
+        self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
+
+        # Expect that the course was added to the shopping cart
+        cart = Order.get_cart_for_user(self.user)
+        self.assertTrue(cart.has_items(PaidCourseRegistration))
+        order_item = PaidCourseRegistration.objects.get(order=cart)
+        self.assertEqual(order_item.course_id, self.course.id)
+
+    def test_auto_enroll_not_accessible(self):
+        # Set the course open date in the future
+        tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
+        self.course.enrollment_start = tomorrow
+        self.update_course(self.course, self.user.id)
+
+        # Finish authentication and try to auto-enroll
+        # This should fail silently, with no exception
+        strategy = self._fake_strategy()
+        strategy.session_set('enroll_course_id', unicode(self.course.id))
+        result = pipeline.change_enrollment(strategy, 1, user=self.user)  # pylint: disable=E1111,E1124
+        self.assertEqual(result, {})
+
+        # Verify that we were NOT enrolled
+        self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
+
+    def test_no_course_id_skips_enroll(self):
+        strategy = self._fake_strategy()
+        result = pipeline.change_enrollment(strategy, 1, user=self.user)  # pylint: disable=E1111,E1124
+        self.assertEqual(result, {})
+        self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
+
+    def _fake_strategy(self):
+        """Simulate the strategy passed to the pipeline step. """
+        request = RequestFactory().get(pipeline.get_complete_url(self.BACKEND_NAME))
+        request.user = self.user
+        request.session = cache.SessionStore()
+
+        return social_utils.load_strategy(
+            backend=self.BACKEND_NAME, request=request
+        )
diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py
index 6907cea26eb..f4f3600df76 100644
--- a/common/djangoapps/third_party_auth/tests/testutil.py
+++ b/common/djangoapps/third_party_auth/tests/testutil.py
@@ -30,8 +30,10 @@ class TestCase(unittest.TestCase):
 
     def setUp(self):
         super(TestCase, self).setUp()
+        self._original_providers = provider.Registry._get_all()
         provider.Registry._reset()
 
     def tearDown(self):
         provider.Registry._reset()
+        provider.Registry.configure_once(self._original_providers)
         super(TestCase, self).tearDown()
diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html
index 49d4559d0fc..8adbc079b9f 100644
--- a/common/templates/course_modes/choose.html
+++ b/common/templates/course_modes/choose.html
@@ -6,9 +6,9 @@
 <%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''}</%block>
 <%block name="pagetitle">
     % if upgrade:
-        ${_("Upgrade Your Registration for {} | Choose Your Track").format(course_name)}
+        ${_("Upgrade Your Enrollment for {} | Choose Your Track").format(course_name)}
     % else:
-        ${_("Register for {} | Choose Your Track").format(course_name)}
+        ${_("Enroll In {} | Choose Your Track").format(course_name)}
     %endif
 </%block>
 
@@ -51,7 +51,7 @@
             <div class=" msg msg-error">
                 <i class="msg-icon icon-warning-sign"></i>
                 <div class="msg-content">
-                    <h3 class="title">${_("Sorry, there was an error when trying to register you")}</h3>
+                    <h3 class="title">${_("Sorry, there was an error when trying to enroll you")}</h3>
                     <div class="copy">
                         <p>${error}</p>
                     </div>
@@ -104,7 +104,7 @@
                                     <ul class="list-actions">
                                         <li class="action action-select">
                                             % if upgrade:
-                                                <input type="submit" name="verified_mode" value="${_('Upgrade Your Registration')}" />
+                                                <input type="submit" name="verified_mode" value="${_('Upgrade Your Enrollment')}" />
                                             % else:
                                                 <input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')}" />
                                             % endif
diff --git a/lms/djangoapps/courseware/features/certificates.py b/lms/djangoapps/courseware/features/certificates.py
index 5e5d0051ccd..3192d7999bc 100644
--- a/lms/djangoapps/courseware/features/certificates.py
+++ b/lms/djangoapps/courseware/features/certificates.py
@@ -69,7 +69,7 @@ def click_verified_track_button():
 def select_verified_track_upgrade(step):
     select_contribution(32)
     world.wait_for_ajax_complete()
-    btn_css = 'input[value="Upgrade Your Registration"]'
+    btn_css = 'input[value="Upgrade Your Enrollment"]'
     world.css_click(btn_css)
     # TODO: might want to change this depending on the changes for upgrade
     assert world.is_css_present('section.progress')
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 69389473030..8019cd1a4c6 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -203,6 +203,17 @@ simplefilter('ignore')  # Change to "default" to see the first instance of each
 ######### Third-party auth ##########
 FEATURES['ENABLE_THIRD_PARTY_AUTH'] = True
 
+THIRD_PARTY_AUTH = {
+    "Google": {
+        "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test",
+        "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test",
+    },
+    "Facebook": {
+        "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test",
+        "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test",
+    },
+}
+
 ################################## OPENID #####################################
 FEATURES['AUTH_USE_OPENID'] = True
 FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
diff --git a/lms/templates/login.html b/lms/templates/login.html
index 6e4267f8367..29db663f65a 100644
--- a/lms/templates/login.html
+++ b/lms/templates/login.html
@@ -206,7 +206,7 @@
 
     % for enabled in provider.Registry.enabled():
       ## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn).
-      <button type="submit" class="button button-primary button-${enabled.NAME}" onclick="thirdPartySignin(event, '${pipeline.get_login_url(enabled.NAME, pipeline.AUTH_ENTRY_LOGIN)}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.NAME)}</button>
+      <button type="submit" class="button button-primary button-${enabled.NAME}" onclick="thirdPartySignin(event, '${pipeline_url[enabled.NAME]}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.NAME)}</button>
     % endfor
 
     </div>
diff --git a/lms/templates/register.html b/lms/templates/register.html
index 13cb548f61e..0033c718391 100644
--- a/lms/templates/register.html
+++ b/lms/templates/register.html
@@ -124,7 +124,7 @@
 
         % for enabled in provider.Registry.enabled():
           ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn).
-          <button type="submit" class="button button-primary button-${enabled.NAME}" onclick="thirdPartySignin(event, '${pipeline.get_login_url(enabled.NAME, pipeline.AUTH_ENTRY_REGISTER)}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign up with {provider_name}').format(provider_name=enabled.NAME)}</button>
+          <button type="submit" class="button button-primary button-${enabled.NAME}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.NAME]}');"><span class="icon ${enabled.ICON_CLASS}"></span>${_('Sign up with {provider_name}').format(provider_name=enabled.NAME)}</button>
         % endfor
 
         </div>
diff --git a/lms/templates/verify_student/_verification_header.html b/lms/templates/verify_student/_verification_header.html
index 82671dd77ff..8a1cdadac22 100644
--- a/lms/templates/verify_student/_verification_header.html
+++ b/lms/templates/verify_student/_verification_header.html
@@ -4,13 +4,13 @@
     <h2 class="title">
         <span class="wrapper-sts">
             % if upgrade:
-                <span class="sts-label">${_("You are upgrading your registration for")}</span>
+                <span class="sts-label">${_("You are upgrading your enrollment for")}</span>
             % elif reverify:
                 <span class="sts-label">${_("You are re-verifying for")}</span>
             % elif modes_dict and "professional" in modes_dict:
                 <span class="sts-label">${_("You are registering for")}</span>
             % else:
-                <span class="sts-label">${_("Congrats! You are now registered to audit")}</span>
+                <span class="sts-label">${_("Congrats! You are now enrolled in the audit track")}</span>
             % endif
                 <span class="sts-course-org">${course_org}'s</span>
                 <span class="sts-course-number">${course_num}</span>
-- 
GitLab