diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py
index eea296c..c2e401d 100644
--- a/common/djangoapps/enrollment/views.py
+++ b/common/djangoapps/enrollment/views.py
@@ -14,7 +14,7 @@ from student.models import NonExistentCourseError, CourseEnrollmentException
 
 
 class EnrollmentUserThrottle(UserRateThrottle):
-        rate = '50/second'  # TODO Limit significantly after performance testing.
+    rate = '50/second'  # TODO Limit significantly after performance testing.
 
 
 @api_view(['GET'])
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 308a068..77468e4 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1034,7 +1034,9 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins
 student_account_js = [
     'js/utils/rwd_header_footer.js',
     'js/utils/edx.utils.validate.js',
-    'js/student_account/enrollment_interface.js',
+    'js/src/utility.js',
+    'js/student_account/enrollment.js',
+    'js/student_account/shoppingcart.js',
     'js/student_account/models/LoginModel.js',
     'js/student_account/models/RegisterModel.js',
     'js/student_account/models/PasswordResetModel.js',
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index 722692a..f081eb3 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -289,9 +289,13 @@
                 exports: 'NotificationView',
                 deps: ['backbone', 'jquery', 'underscore']
             },
-            'js/student_account/enrollment_interface': {
+            'js/student_account/enrollment': {
                 exports: 'edx.student.account.EnrollmentInterface',
-                deps: ['jquery', 'jquery.cookie', 'underscore', 'gettext']
+                deps: ['jquery', 'jquery.cookie']
+            },
+            'js/student_account/shoppingcart': {
+                exports: 'edx.student.account.ShoppingCartInterface',
+                deps: ['jquery', 'jquery.cookie', 'underscore']
             },
             // Student account registration/login
             // Loaded explicitly until these are converted to RequireJS
@@ -350,13 +354,16 @@
                     'underscore',
                     'backbone',
                     'gettext',
+                    'utility',
                     'js/student_account/views/LoginView',
                     'js/student_account/views/PasswordResetView',
                     'js/student_account/views/RegisterView',
                     'js/student_account/models/LoginModel',
                     'js/student_account/models/PasswordResetModel',
                     'js/student_account/models/RegisterModel',
-                    'js/student_account/views/FormView'
+                    'js/student_account/views/FormView',
+                    'js/student_account/enrollment',
+                    'js/student_account/shoppingcart',
                 ]
             }
         }
@@ -375,7 +382,8 @@
         'lms/include/js/spec/student_account/login_spec.js',
         'lms/include/js/spec/student_account/register_spec.js',
         'lms/include/js/spec/student_account/password_reset_spec.js',
-        'lms/include/js/spec/student_account/enrollment_interface_spec.js',
+        'lms/include/js/spec/student_account/enrollment_spec.js',
+        'lms/include/js/spec/student_account/shoppingcart_spec.js',
         'lms/include/js/spec/student_profile/profile_spec.js'
     ]);
 
diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js
index 8a9cfb7..896ef75 100644
--- a/lms/static/js/spec/student_account/access_spec.js
+++ b/lms/static/js/spec/student_account/access_spec.js
@@ -3,8 +3,10 @@ define([
     'js/common_helpers/template_helpers',
     'js/common_helpers/ajax_helpers',
     'js/student_account/views/AccessView',
-    'js/student_account/views/FormView'
-], function($, TemplateHelpers, AjaxHelpers, AccessView) {
+    'js/student_account/views/FormView',
+    'js/student_account/enrollment',
+    'js/student_account/shoppingcart'
+], function($, TemplateHelpers, AjaxHelpers, AccessView, FormView, EnrollmentInterface, ShoppingCartInterface) {
         describe('edx.student.account.AccessView', function() {
             'use strict';
 
@@ -51,7 +53,9 @@ define([
                             }
                         }
                     ]
-                };
+                },
+                FORWARD_URL = '/courseware/next',
+                COURSE_KEY = 'edx/DemoX/Fall';
 
             var ajaxAssertAndRespond = function(url, requestIndex) {
                 // Verify that the client contacts the server as expected
@@ -77,6 +81,14 @@ define([
                     platformName: 'edX'
                 });
 
+                // Mock the redirect call
+                spyOn( view, 'redirect' ).andCallFake( function() {} );
+
+                // Mock the enrollment and shopping cart interfaces
+                spyOn( EnrollmentInterface, 'enroll' ).andCallFake( function() {} );
+                spyOn( ShoppingCartInterface, 'addCourseToCart' ).andCallFake( function() {} );
+
+                // Initialize the subview
                 ajaxAssertAndRespond(AJAX_INFO[mode].url);
             };
 
@@ -97,6 +109,20 @@ define([
                 ajaxAssertAndRespond(AJAX_INFO[type].url, AJAX_INFO[type].requestIndex);
             };
 
+            /**
+             * Simulate query string params.
+             *
+             * @param {object} params Parameters to set, each of which
+             * should be prefixed with '?'
+             */
+            var setFakeQueryParams = function( params ) {
+                spyOn( $, 'url' ).andCallFake(function( requestedParam ) {
+                    if ( params.hasOwnProperty(requestedParam) ) {
+                        return params[requestedParam];
+                    }
+                });
+            };
+
             beforeEach(function() {
                 setFixtures('<div id="login-and-registration-container"></div>');
                 TemplateHelpers.installTemplate('templates/student_account/access');
@@ -104,6 +130,11 @@ define([
                 TemplateHelpers.installTemplate('templates/student_account/register');
                 TemplateHelpers.installTemplate('templates/student_account/password_reset');
                 TemplateHelpers.installTemplate('templates/student_account/form_field');
+
+                // Stub analytics tracking
+                // TODO: use RequireJS to ensure that this is loaded correctly
+                window.analytics = window.analytics || {};
+                window.analytics.track = window.analytics.track || function() {};
             });
 
             it('can initially display the login form', function() {
@@ -143,14 +174,83 @@ define([
                 view.resetPassword();
 
                 ajaxAssertAndRespond(
-                    AJAX_INFO['password_reset'].url,
-                    AJAX_INFO['password_reset'].requestIndex
+                    AJAX_INFO.password_reset.url,
+                    AJAX_INFO.password_reset.requestIndex
                 );
 
                 // Verify that the password reset wrapper is populated
                 expect($('#password-reset-wrapper')).not.toBeEmpty();
             });
 
+            it('enrolls the user on auth complete', function() {
+                ajaxSpyAndInitialize(this, 'login');
+
+                // Simulate providing enrollment query string params
+                setFakeQueryParams({
+                    '?enrollment_action': 'enroll',
+                    '?course_id': COURSE_KEY
+                });
+
+                // Trigger auth complete on the login view
+                view.subview.login.trigger('auth-complete');
+
+                // Expect that the view tried to enroll the student
+                expect( EnrollmentInterface.enroll ).toHaveBeenCalledWith( COURSE_KEY );
+            });
+
+            it('adds a white-label course to the shopping cart on auth complete', function() {
+                ajaxSpyAndInitialize(this, 'register');
+
+                // Simulate providing "add to cart" query string params
+                setFakeQueryParams({
+                    '?enrollment_action': 'add_to_cart',
+                    '?course_id': COURSE_KEY
+                });
+
+                // Trigger auth complete on the register view
+                view.subview.register.trigger('auth-complete');
+
+                // Expect that the view tried to add the course to the user's shopping cart
+                expect( ShoppingCartInterface.addCourseToCart ).toHaveBeenCalledWith( COURSE_KEY );
+            });
+
+            it('redirects the user to the dashboard on auth complete', function() {
+                ajaxSpyAndInitialize(this, 'register');
+
+                // Trigger auth complete
+                view.subview.register.trigger('auth-complete');
+
+                // Since we did not provide a ?next query param, expect a redirect to the dashboard.
+                expect( view.redirect ).toHaveBeenCalledWith( '/dashboard' );
+            });
+
+            it('redirects the user to the next page on auth complete', function() {
+                ajaxSpyAndInitialize(this, 'register');
+
+                // Simulate providing a ?next query string parameter
+                setFakeQueryParams({ '?next': FORWARD_URL });
+
+                // Trigger auth complete
+                view.subview.register.trigger('auth-complete');
+
+                // Verify that we were redirected
+                expect( view.redirect ).toHaveBeenCalledWith( FORWARD_URL );
+            });
+
+            it('ignores redirect to external URLs', function() {
+                ajaxSpyAndInitialize(this, 'register');
+
+                // Simulate providing a ?next query string parameter
+                // that goes to an external URL
+                setFakeQueryParams({ '?next': "http://www.example.com" });
+
+                // Trigger auth complete
+                view.subview.register.trigger('auth-complete');
+
+                // Expect that we ignore the external URL and redirect to the dashboard
+                expect( view.redirect ).toHaveBeenCalledWith( "/dashboard" );
+            });
+
             it('displays an error if a form definition could not be loaded', function() {
                 // Spy on AJAX requests
                 requests = AjaxHelpers.requests(this);
diff --git a/lms/static/js/spec/student_account/enrollment_interface_spec.js b/lms/static/js/spec/student_account/enrollment_interface_spec.js
deleted file mode 100644
index 204861e..0000000
--- a/lms/static/js/spec/student_account/enrollment_interface_spec.js
+++ /dev/null
@@ -1,14 +0,0 @@
-define(['js/student_account/enrollment_interface'],
-    function(EnrollmentInterface) {
-        describe("edx.student.account.EnrollmentInterface", function() {
-            'use strict';
-
-            it('checks if a given course mode slug exists in an array of mode objects', function() {
-                var courseModes = [ { slug: 'honor' }, { slug: 'professional' } ]
-
-                expect( EnrollmentInterface.modeInArray( courseModes, 'professional' ) ).toBe(true);
-                expect( EnrollmentInterface.modeInArray( courseModes, 'audit' ) ).toBe(false);
-            });
-        });
-    }
-);
diff --git a/lms/static/js/spec/student_account/enrollment_spec.js b/lms/static/js/spec/student_account/enrollment_spec.js
new file mode 100644
index 0000000..05119f1
--- /dev/null
+++ b/lms/static/js/spec/student_account/enrollment_spec.js
@@ -0,0 +1,49 @@
+define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
+    function( AjaxHelpers, EnrollmentInterface ) {
+        'use strict';
+
+        describe( 'edx.student.account.EnrollmentInterface', function() {
+
+            var COURSE_KEY = 'edX/DemoX/Fall',
+                ENROLL_URL = '/enrollment/v0/course/edX/DemoX/Fall',
+                FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/';
+
+            beforeEach(function() {
+                // Mock the redirect call
+                spyOn(EnrollmentInterface, 'redirect').andCallFake(function() {});
+            });
+
+            it('enrolls a user in a course', function() {
+                // Spy on Ajax requests
+                var requests = AjaxHelpers.requests( this );
+
+                // Attempt to enroll the user
+                EnrollmentInterface.enroll( COURSE_KEY );
+
+                // Expect that the correct request was made to the server
+                AjaxHelpers.expectRequest( requests, 'POST', ENROLL_URL );
+
+                // Simulate a successful response from the server
+                AjaxHelpers.respondWithJson(requests, {});
+
+                // Verify that the user was redirected correctly
+                expect( EnrollmentInterface.redirect ).toHaveBeenCalledWith( FORWARD_URL );
+            });
+
+            it('redirects the user if enrollment fails', function() {
+                // Spy on Ajax requests
+                var requests = AjaxHelpers.requests( this );
+
+                // Attempt to enroll the user
+                EnrollmentInterface.enroll( COURSE_KEY );
+
+                // Simulate an error response from the server
+                AjaxHelpers.respondWithError(requests);
+
+                // Verify that the user was still redirected
+                expect(EnrollmentInterface.redirect).toHaveBeenCalledWith( FORWARD_URL );
+            });
+
+        });
+    }
+);
diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js
index ec75902..32fd8e1 100644
--- a/lms/static/js/spec/student_account/login_spec.js
+++ b/lms/static/js/spec/student_account/login_spec.js
@@ -12,6 +12,7 @@ define([
             var model = null,
                 view = null,
                 requests = null,
+                authComplete = false,
                 PLATFORM_NAME = 'edX',
                 USER_DATA = {
                     email: 'xsy@edx.org',
@@ -72,7 +73,10 @@ define([
 
             var createLoginView = function(test) {
                 // Initialize the login model
-                model = new LoginModel({ url: FORM_DESCRIPTION.submit_url });
+                model = new LoginModel({}, {
+                    url: FORM_DESCRIPTION.submit_url,
+                    method: FORM_DESCRIPTION.method
+                });
 
                 // Initialize the login view
                 view = new LoginView({
@@ -85,9 +89,10 @@ define([
                 // Spy on AJAX requests
                 requests = AjaxHelpers.requests(test);
 
-                // Mock out redirection logic
-                spyOn(view, 'redirect').andCallFake(function() {
-                    return true;
+                // Intercept events from the view
+                authComplete = false;
+                view.on("auth-complete", function() {
+                    authComplete = true;
                 });
             };
 
@@ -130,16 +135,16 @@ define([
 
                 // Verify that the client contacts the server with the expected data
                 AjaxHelpers.expectRequest(
-                    requests, 'POST', FORM_DESCRIPTION.submit_url, $.param(
-                        $.extend({url: FORM_DESCRIPTION.submit_url}, USER_DATA)
-                    )
+                    requests, 'POST',
+                    FORM_DESCRIPTION.submit_url,
+                    $.param( USER_DATA )
                 );
 
                 // Respond with status code 200
                 AjaxHelpers.respondWithJson(requests, {});
 
-                // Verify that the user is redirected to the dashboard
-                expect(view.redirect).toHaveBeenCalledWith('/dashboard');
+                // Verify that auth-complete is triggered
+                expect(authComplete).toBe(true);
             });
 
             it('displays third-party auth login buttons', function() {
@@ -175,6 +180,9 @@ define([
 
                 // Verify that submission errors are visible
                 expect(view.$errors).not.toHaveClass('hidden');
+
+                // Expect auth complete NOT to have been triggered
+                expect(authComplete).toBe(false);
             });
 
             it('displays an error if the server returns an error while logging in', function() {
@@ -186,9 +194,9 @@ define([
                 // Simulate an error from the LMS servers
                 AjaxHelpers.respondWithError(requests);
 
-                // Expect that an error is displayed, and that we haven't been redirected
+                // Expect that an error is displayed and that auth complete is not triggered
                 expect(view.$errors).not.toHaveClass('hidden');
-                expect(view.redirect).not.toHaveBeenCalled();
+                expect(authComplete).toBe(false);
 
                 // If we try again and succeed, the error should go away
                 submitForm();
@@ -196,8 +204,9 @@ define([
                 // This time, respond with status code 200
                 AjaxHelpers.respondWithJson(requests, {});
 
-                // Expect that the error is hidden
+                // Expect that the error is hidden and auth complete is triggered
                 expect(view.$errors).toHaveClass('hidden');
+                expect(authComplete).toBe(true);
             });
         });
     }
diff --git a/lms/static/js/spec/student_account/password_reset_spec.js b/lms/static/js/spec/student_account/password_reset_spec.js
index 78128c9..37a6ab4 100644
--- a/lms/static/js/spec/student_account/password_reset_spec.js
+++ b/lms/static/js/spec/student_account/password_reset_spec.js
@@ -30,7 +30,10 @@ define([
 
             var createPasswordResetView = function(that) {
                 // Initialize the password reset model
-                model = new PasswordResetModel({ url: FORM_DESCRIPTION.submit_url });
+                model = new PasswordResetModel({}, {
+                    url: FORM_DESCRIPTION.submit_url,
+                    method: FORM_DESCRIPTION.method
+                });
 
                 // Initialize the password reset view
                 view = new PasswordResetView({
@@ -77,10 +80,9 @@ define([
 
                 // Verify that the client contacts the server with the expected data
                 AjaxHelpers.expectRequest(
-                    requests, 'POST', FORM_DESCRIPTION.submit_url, $.param({
-                        url: FORM_DESCRIPTION.submit_url,
-                        email: EMAIL
-                    })
+                    requests, 'POST',
+                    FORM_DESCRIPTION.submit_url,
+                    $.param({ email: EMAIL })
                 );
 
                 // Respond with status code 200
@@ -125,10 +127,10 @@ define([
 
                 // If we try again and succeed, the error should go away
                 submitEmail();
-                
+
                 // This time, respond with status code 200
                 AjaxHelpers.respondWithJson(requests, {});
-                
+
                 // Expect that the error is hidden
                 expect(view.$errors).toHaveClass('hidden');
             });
diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js
index fcc0ceb..2c0d849 100644
--- a/lms/static/js/spec/student_account/register_spec.js
+++ b/lms/static/js/spec/student_account/register_spec.js
@@ -12,6 +12,7 @@ define([
             var model = null,
                 view = null,
                 requests = null,
+                authComplete = false,
                 PLATFORM_NAME = 'edX',
                 USER_DATA = {
                     email: 'xsy@edx.org',
@@ -160,7 +161,10 @@ define([
 
             var createRegisterView = function(that) {
                 // Initialize the register model
-                model = new RegisterModel({ url: FORM_DESCRIPTION.submit_url });
+                model = new RegisterModel({}, {
+                    url: FORM_DESCRIPTION.submit_url,
+                    method: FORM_DESCRIPTION.method
+                });
 
                 // Initialize the register view
                 view = new RegisterView({
@@ -173,9 +177,10 @@ define([
                 // Spy on AJAX requests
                 requests = AjaxHelpers.requests(that);
 
-                // Mock out redirection logic
-                spyOn(view, 'redirect').andCallFake(function() {
-                    return true;
+                // Intercept events from the view
+                authComplete = false;
+                view.on("auth-complete", function() {
+                    authComplete = true;
                 });
             };
 
@@ -225,16 +230,16 @@ define([
 
                 // Verify that the client contacts the server with the expected data
                 AjaxHelpers.expectRequest(
-                    requests, 'POST', FORM_DESCRIPTION.submit_url, $.param(
-                        $.extend({url: FORM_DESCRIPTION.submit_url}, USER_DATA)
-                    )
+                    requests, 'POST',
+                    FORM_DESCRIPTION.submit_url,
+                    $.param( USER_DATA )
                 );
 
                 // Respond with status code 200
                 AjaxHelpers.respondWithJson(requests, {});
 
-                // Verify that the user is redirected to the dashboard
-                expect(view.redirect).toHaveBeenCalledWith('/dashboard');
+                // Verify that auth complete is triggered
+                expect(authComplete).toBe(true);
             });
 
             it('displays third-party auth registration buttons', function() {
@@ -269,6 +274,9 @@ define([
 
                 // Verify that submission errors are visible
                 expect(view.$errors).not.toHaveClass('hidden');
+
+                // Expect that auth complete is NOT triggered
+                expect(authComplete).toBe(false);
             });
 
             it('displays an error if the server returns an error while registering', function() {
@@ -280,8 +288,9 @@ define([
                 // Simulate an error from the LMS servers
                 AjaxHelpers.respondWithError(requests);
 
-                // Expect that an error is displayed
+                // Expect that an error is displayed and that auth complete is NOT triggered
                 expect(view.$errors).not.toHaveClass('hidden');
+                expect(authComplete).toBe(false);
 
                 // If we try again and succeed, the error should go away
                 submitForm();
@@ -289,8 +298,9 @@ define([
                 // This time, respond with status code 200
                 AjaxHelpers.respondWithJson(requests, {});
 
-                // Expect that the error is hidden
+                // Expect that the error is hidden and that auth complete is triggered
                 expect(view.$errors).toHaveClass('hidden');
+                expect(authComplete).toBe(true);
             });
         });
     }
diff --git a/lms/static/js/spec/student_account/shoppingcart_spec.js b/lms/static/js/spec/student_account/shoppingcart_spec.js
new file mode 100644
index 0000000..5936f76
--- /dev/null
+++ b/lms/static/js/spec/student_account/shoppingcart_spec.js
@@ -0,0 +1,48 @@
+define(['js/common_helpers/ajax_helpers', 'js/student_account/shoppingcart'],
+    function(AjaxHelpers, ShoppingCartInterface) {
+        'use strict';
+
+        describe( 'edx.student.account.ShoppingCartInterface', function() {
+
+            var COURSE_KEY = "edX/DemoX/Fall",
+                ADD_COURSE_URL = "/shoppingcart/add/course/edX/DemoX/Fall/",
+                FORWARD_URL = "/shoppingcart/";
+
+            beforeEach(function() {
+                // Mock the redirect call
+                spyOn(ShoppingCartInterface, 'redirect').andCallFake(function() {});
+            });
+
+            it('adds a course to the cart', function() {
+                // Spy on Ajax requests
+                var requests = AjaxHelpers.requests( this );
+
+                // Attempt to add a course to the cart
+                ShoppingCartInterface.addCourseToCart( COURSE_KEY );
+
+                // Expect that the correct request was made to the server
+                AjaxHelpers.expectRequest( requests, 'POST', ADD_COURSE_URL );
+
+                // Simulate a successful response from the server
+                AjaxHelpers.respondWithJson( requests, {} );
+
+                // Expect that the user was redirected to the shopping cart
+                expect( ShoppingCartInterface.redirect ).toHaveBeenCalledWith( FORWARD_URL );
+            });
+
+            it('redirects the user on a server error', function() {
+                // Spy on Ajax requests
+                var requests = AjaxHelpers.requests( this );
+
+                // Attempt to add a course to the cart
+                ShoppingCartInterface.addCourseToCart( COURSE_KEY );
+
+                // Simulate an error response from the server
+                AjaxHelpers.respondWithError( requests );
+
+                // Expect that the user was redirected to the shopping cart
+                expect( ShoppingCartInterface.redirect ).toHaveBeenCalledWith( FORWARD_URL );
+            });
+        });
+    }
+);
diff --git a/lms/static/js/student_account/enrollment.js b/lms/static/js/student_account/enrollment.js
new file mode 100644
index 0000000..f16e78f
--- /dev/null
+++ b/lms/static/js/student_account/enrollment.js
@@ -0,0 +1,63 @@
+var edx = edx || {};
+
+(function($) {
+    'use strict';
+
+    edx.student = edx.student || {};
+    edx.student.account = edx.student.account || {};
+
+    edx.student.account.EnrollmentInterface = {
+
+        urls: {
+            course: '/enrollment/v0/course/',
+            trackSelection: '/course_modes/choose/'
+        },
+
+        headers: {
+            'X-CSRFToken': $.cookie('csrftoken')
+        },
+
+        /**
+         * Enroll a user in a course, then redirect the user
+         * to the track selection page.
+         * @param  {string} courseKey  Slash-separated course key.
+         */
+        enroll: function( courseKey ) {
+            $.ajax({
+                url: this.courseEnrollmentUrl( courseKey ),
+                type: 'POST',
+                data: {},
+                headers: this.headers,
+                context: this
+            }).always(function() {
+                this.redirect( this.trackSelectionUrl( courseKey ) );
+            });
+        },
+
+        /**
+         * Construct the URL to the track selection page for a course.
+         * @param  {string} courseKey Slash-separated course key.
+         * @return {string} The URL to the track selection page.
+         */
+        trackSelectionUrl: function( courseKey ) {
+            return this.urls.trackSelection + courseKey + '/';
+        },
+
+        /**
+         * Construct a URL to enroll in a course.
+         * @param  {string} courseKey Slash-separated course key.
+         * @return {string} The URL to enroll in a course.
+         */
+        courseEnrollmentUrl: function( courseKey ) {
+            return this.urls.course + courseKey;
+        },
+
+        /**
+         * Redirect to a URL.  Mainly useful for mocking out in tests.
+         * @param  {string} url The URL to redirect to.
+         */
+        redirect: function(url) {
+            window.location.href = url;
+        }
+    };
+})(jQuery);
diff --git a/lms/static/js/student_account/enrollment_interface.js b/lms/static/js/student_account/enrollment_interface.js
deleted file mode 100644
index 3572458..0000000
--- a/lms/static/js/student_account/enrollment_interface.js
+++ /dev/null
@@ -1,87 +0,0 @@
-var edx = edx || {};
-
-(function($, _, gettext) {
-    'use strict';
-
-    edx.student = edx.student || {};
-    edx.student.account = edx.student.account || {};
-
-    edx.student.account.EnrollmentInterface = {
-        courseUrl: '/enrollment/v0/course/',
-
-        studentUrl: '/enrollment/v0/student',
-
-        trackSelectionUrl: '/course_modes/choose/',
-
-        headers: {
-            'X-CSRFToken': $.cookie('csrftoken')
-        },
-        
-        studentInformation: function(courseKey) {
-            // retrieve student enrollment information
-        },
-
-        courseInformation: function(courseKey) {
-            // retrieve course information from the enrollment API
-        },
-
-        modeInArray: function(modeObjects, targetMode) {
-            // Check if a given course mode slug exists in an array of mode objects
-            var result = _.find(modeObjects, function(mode) {
-                return mode.slug === targetMode; 
-            });
-
-            /* _.find returns the first value which passes the provided truth test,
-            /* or undefined if no values pass the test
-             */
-            return !_.isUndefined(result);
-        },
-
-        enroll: function(courseKey, forwardUrl){
-            var me = this;
-            // attempt to enroll a student in a course
-            $.ajax({
-                url: this.courseUrl + courseKey,
-                type: 'POST',
-                data: {},
-                headers: this.headers
-            }).done(function(data){
-                me.postEnrollmentHandler(courseKey, data, forwardUrl);
-            }
-            ).fail(function(data, textStatus) {
-                me.enrollmentFailureHandler(courseKey, data, forwardUrl);
-            });
-        },
-
-        enrollmentFailureHandler: function(courseKey, data, forwardUrl) {
-            // handle failures to enroll via the API
-            if(data.status == 400) {
-                /* This status code probably means we don't have permissions to register
-                /* for this course; look at the contents of the response
-                 */
-                var course = $.parseJSON(data.responseText);
-                // see if it's a professional ed course
-                if( 'course_modes' in course && this.modeInArray(course.course_modes, 'professional') ) {
-                    // forward appropriately
-                    forwardUrl = this.trackSelectionUrl + courseKey;
-                }
-            }
-            // TODO: if we have a paid registration mode, add item to the cart and send them along
-
-            // TODO: we should figure out how to handle errors here
-            window.location.href = forwardUrl;
-        },
-
-        postEnrollmentHandler: function(courseKey, data, forwardUrl) {
-            // Determine whether or not the course needs to be redirected to
-            // a particular page.
-            var course = data.course,
-                course_modes = course.course_modes;
-            
-            // send the user to the track selection page, because it will do the right thing
-            forwardUrl = this.trackSelectionUrl + courseKey;
-
-            window.location.href = forwardUrl;
-        }
-    };
-})(jQuery, _, gettext);
diff --git a/lms/static/js/student_account/models/LoginModel.js b/lms/static/js/student_account/models/LoginModel.js
index e131695..0b0124f 100644
--- a/lms/static/js/student_account/models/LoginModel.js
+++ b/lms/static/js/student_account/models/LoginModel.js
@@ -18,9 +18,9 @@ var edx = edx || {};
 
         urlRoot: '',
 
-        initialize: function( obj ) {
-            this.ajaxType = obj.method;
-            this.urlRoot = obj.url;
+        initialize: function( attributes, options ) {
+            this.ajaxType = options.method;
+            this.urlRoot = options.url;
         },
 
         sync: function(method, model) {
diff --git a/lms/static/js/student_account/models/PasswordResetModel.js b/lms/static/js/student_account/models/PasswordResetModel.js
index d43e591..60858dc 100644
--- a/lms/static/js/student_account/models/PasswordResetModel.js
+++ b/lms/static/js/student_account/models/PasswordResetModel.js
@@ -16,9 +16,9 @@ var edx = edx || {};
 
         urlRoot: '',
 
-        initialize: function( obj ) {
-            this.ajaxType = obj.method;
-            this.urlRoot = obj.url;
+        initialize: function( attributes, options ) {
+            this.ajaxType = options.method;
+            this.urlRoot = options.url;
         },
 
         sync: function(method, model) {
diff --git a/lms/static/js/student_account/models/RegisterModel.js b/lms/static/js/student_account/models/RegisterModel.js
index e5a009f..7e07e85 100644
--- a/lms/static/js/student_account/models/RegisterModel.js
+++ b/lms/static/js/student_account/models/RegisterModel.js
@@ -18,16 +18,15 @@ var edx = edx || {};
             year_of_birth: '',
             mailing_address: '',
             goals: '',
-            honor_code: false
         },
 
         ajaxType: '',
 
         urlRoot: '',
 
-        initialize: function( obj ) {
-            this.ajaxType = obj.method;
-            this.urlRoot = obj.url;
+        initialize: function( attributes, options ) {
+            this.ajaxType = options.method;
+            this.urlRoot = options.url;
         },
 
         sync: function(method, model) {
diff --git a/lms/static/js/student_account/shoppingcart.js b/lms/static/js/student_account/shoppingcart.js
new file mode 100644
index 0000000..18ec197
--- /dev/null
+++ b/lms/static/js/student_account/shoppingcart.js
@@ -0,0 +1,49 @@
+/**
+* Use the shopping cart to purchase courses.
+*/
+
+var edx = edx || {};
+
+(function($) {
+    'use strict';
+
+    edx.student = edx.student || {};
+    edx.student.account = edx.student.account || {};
+
+    edx.student.account.ShoppingCartInterface = {
+
+        urls: {
+            viewCart: "/shoppingcart/",
+            addCourse: "/shoppingcart/add/course/"
+        },
+
+        headers: {
+            'X-CSRFToken': $.cookie('csrftoken')
+        },
+
+        /**
+         * Add a course to a cart, then redirect to the view cart page.
+         * @param {string} courseId The slash-separated course ID to add to the cart.
+         */
+        addCourseToCart: function( courseId ) {
+            $.ajax({
+                url: this.urls.addCourse + courseId + "/",
+                type: 'POST',
+                data: {},
+                headers: this.headers,
+                context: this
+            }).always(function() {
+                this.redirect( this.urls.viewCart );
+            });
+        },
+
+        /**
+         * Redirect to a URL.  Mainly useful for mocking out in tests.
+         * @param  {string} url The URL to redirect to.
+         */
+        redirect: function( url ) {
+            window.location.href = url;
+        }
+    };
+
+})(jQuery);
diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js
index 4060d53..8b5debc 100644
--- a/lms/static/js/student_account/views/AccessView.js
+++ b/lms/static/js/student_account/views/AccessView.js
@@ -64,7 +64,7 @@ var edx = edx || {};
 
         load: {
             login: function( data, context ) {
-                var model = new edx.student.account.LoginModel({
+                var model = new edx.student.account.LoginModel({}, {
                     method: data.method,
                     url: data.submit_url
                 });
@@ -78,10 +78,14 @@ var edx = edx || {};
 
                 // Listen for 'password-help' event to toggle sub-views
                 context.listenTo( context.subview.login, 'password-help', context.resetPassword );
+
+                // Listen for 'auth-complete' event so we can enroll/redirect the user appropriately.
+                context.listenTo( context.subview.login, 'auth-complete', context.authComplete );
+
             },
 
             reset: function( data, context ) {
-                var model = new edx.student.account.PasswordResetModel({
+                var model = new edx.student.account.PasswordResetModel({}, {
                     method: data.method,
                     url: data.submit_url
                 });
@@ -93,7 +97,7 @@ var edx = edx || {};
             },
 
             register: function( data, context ) {
-                var model = new edx.student.account.RegisterModel({
+                var model = new edx.student.account.RegisterModel({}, {
                     method: data.method,
                     url: data.submit_url
                 });
@@ -104,6 +108,9 @@ var edx = edx || {};
                     thirdPartyAuth: context.thirdPartyAuth,
                     platformName: context.platformName
                 });
+
+                // Listen for 'auth-complete' event so we can enroll/redirect the user appropriately.
+                context.listenTo( context.subview.register, 'auth-complete', context.authComplete );
             }
         },
 
@@ -162,6 +169,92 @@ var edx = edx || {};
             },'slow');
         },
 
+        /**
+         * Once authentication has completed successfully, a user may need to:
+         *
+         * - Enroll in a course.
+         * - Add a course to the shopping cart.
+         * - Be redirected to the dashboard / track selection page / shopping cart.
+         *
+         * This handler is triggered upon successful authentication,
+         * either from the login or registration form.  It checks
+         * query string params, performs enrollment/shopping cart actions,
+         * then redirects the user to the next page.
+         *
+         * The optional query string params are:
+         *
+         * ?next: If provided, redirect to this page upon successful auth.
+         *   Django uses this when an unauthenticated user accesses a view
+         *   decorated with @login_required.
+         *
+         * ?enrollment_action: Can be either "enroll" or "add_to_cart".
+         *   If you provide this param, you must also provide a `course_id` param;
+         *   otherwise, no action will be taken.
+         *
+         * ?course_id: The slash-separated course ID to enroll in or add to the cart.
+         *
+         */
+        authComplete: function() {
+            var enrollment = edx.student.account.EnrollmentInterface,
+                shoppingcart = edx.student.account.ShoppingCartInterface,
+                redirectUrl = '/dashboard',
+                queryParams = this.queryParams();
+
+            if ( queryParams.enrollmentAction === 'enroll' && queryParams.courseId) {
+                /*
+                If we need to enroll in a course, mark as enrolled.
+                The enrollment interface will redirect the student once enrollment completes.
+                */
+                enrollment.enroll( decodeURIComponent( queryParams.courseId ) );
+            } else if ( queryParams.enrollmentAction === 'add_to_cart' && queryParams.courseId) {
+                /*
+                If this is a paid course, add it to the shopping cart and redirect
+                the user to the "view cart" page.
+                */
+                shoppingcart.addCourseToCart( decodeURIComponent( queryParams.courseId ) );
+            } else {
+                /*
+                Otherwise, redirect the user to the next page
+                Check for forwarding url and ensure that it isn't external.
+                If not, use the default forwarding URL.
+                */
+                if ( !_.isNull( queryParams.next ) ) {
+                    var next = decodeURIComponent( queryParams.next );
+
+                    // Ensure that the URL is internal for security reasons
+                    if ( !window.isExternal( next ) ) {
+                        redirectUrl = next;
+                    }
+                }
+
+                this.redirect( redirectUrl );
+            }
+        },
+
+        /**
+         * Redirect to a URL.  Mainly useful for mocking out in tests.
+         * @param  {string} url The URL to redirect to.
+         */
+        redirect: function( url ) {
+            window.location.href = url;
+        },
+
+        /**
+         * Retrieve query params that we use post-authentication
+         * to decide whether to enroll a student in a course, add
+         * an item to the cart, or redirect.
+         *
+         * @return {object} The query params.  If any param is not
+         * provided, it will default to null.
+         */
+        queryParams: function() {
+            return {
+                next: $.url( '?next' ),
+                enrollmentAction: $.url( '?enrollment_action' ),
+                courseId: $.url( '?course_id' )
+            };
+        },
+
         form: {
             isLoaded: function( $form ) {
                 return $form.html().length > 0;
diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js
index b10e4d6..23df7bf 100644
--- a/lms/static/js/student_account/views/LoginView.js
+++ b/lms/static/js/student_account/views/LoginView.js
@@ -79,29 +79,7 @@ var edx = edx || {};
         },
 
         saveSuccess: function () {
-            var enrollment = edx.student.account.EnrollmentInterface,
-                redirectUrl = '/dashboard',
-                next = null;
-
-            // Check for forwarding url
-            if ( !_.isNull( $.url('?next') ) ) {
-                next = decodeURIComponent( $.url('?next') );
-
-                if ( !window.isExternal(next) ) {
-                    redirectUrl = next;
-                }
-            }
-
-            // If we need to enroll in a course, mark as enrolled
-            if ( $.url('?enrollment_action') === 'enroll' ) {
-                enrollment.enroll( decodeURIComponent( $.url('?course_id') ), redirectUrl );
-            } else {
-                this.redirect(redirectUrl);
-            }
-        },
-
-        redirect: function( url ) {
-            window.location.href = url;
+            this.trigger('auth-complete');
         },
 
         saveError: function( error ) {
diff --git a/lms/static/js/student_account/views/PasswordResetView.js b/lms/static/js/student_account/views/PasswordResetView.js
index da85293..40af682 100644
--- a/lms/static/js/student_account/views/PasswordResetView.js
+++ b/lms/static/js/student_account/views/PasswordResetView.js
@@ -1,6 +1,6 @@
 var edx = edx || {};
 
-(function($, _, gettext) {
+(function($, gettext) {
     'use strict';
 
     edx.student = edx.student || {};
@@ -39,4 +39,4 @@ var edx = edx || {};
         }
     });
 
-})(jQuery, _, gettext);
+})(jQuery, gettext);
diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js
index afa250d..2fd857f 100644
--- a/lms/static/js/student_account/views/RegisterView.js
+++ b/lms/static/js/student_account/views/RegisterView.js
@@ -55,25 +55,7 @@ var edx = edx || {};
         },
 
         saveSuccess: function() {
-            var enrollment = edx.student.account.EnrollmentInterface,
-                redirectUrl = '/dashboard',
-                next = null;
-
-            // Check for forwarding url
-            if ( !_.isNull( $.url('?next') ) ) {
-                next = decodeURIComponent( $.url('?next') );
-
-                if ( !window.isExternal(next) ) {
-                    redirectUrl = next;
-                }
-            }
-
-            // If we need to enroll in a course, mark as enrolled
-            if ( $.url('?enrollment_action') === 'enroll' ) {
-                enrollment.enroll( decodeURIComponent( $.url('?course_id') ), redirectUrl );
-            } else {
-                this.redirect(redirectUrl);
-            }
+            this.trigger('auth-complete');
         },
 
         redirect: function( url ) {