diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index 8a165e8be4c267c0b059a33cb7b44f8efec840f8..2f23b2cd9e03e705dd405bfcdb54d3c750acfdb5 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -224,7 +224,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
         progress = reduce(Progress.add_counts, progresses, None)
         return progress
 
-    def handle_ajax(self, dispatch, data):  # TODO: bounds checking
+    def handle_ajax(self, dispatch, data, view=STUDENT_VIEW):  # TODO: bounds checking
         ''' get = request.POST instance '''
         if dispatch == 'goto_position':
             # set position to default value if either 'position' argument not
@@ -263,7 +263,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
                 else:
                     # check if prerequisite has been met
                     prereq_met, prereq_meta_info = self._compute_is_prereq_met(True)
-            meta = self._get_render_metadata(context, display_items, prereq_met, prereq_meta_info, banner_text, STUDENT_VIEW)
+            meta = self._get_render_metadata(context, display_items, prereq_met, prereq_meta_info, banner_text, view)
             meta['display_name'] = self.display_name_with_default
             meta['format'] = getattr(self, 'format', '')
             return json.dumps(meta)
diff --git a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py
index 7a6a8975b75009349b1f8826f37e735c2137d6c9..111aad28178e1c50a2923846f950b27d37083143 100644
--- a/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py
+++ b/lms/djangoapps/course_home_api/outline/v1/tests/test_views.py
@@ -96,7 +96,18 @@ class OutlineTabTestViews(BaseCourseHomeTests):
     def test_get_unauthenticated_user(self):
         self.client.logout()
         response = self.client.get(self.url)
-        self.assertEqual(response.status_code, 403)
+        self.assertEqual(response.status_code, 200)
+
+        course_blocks = response.data.get('course_blocks')
+        self.assertEqual(course_blocks, None)
+
+        course_tools = response.data.get('course_tools')
+        self.assertEqual(len(course_tools), 0)
+
+        dates_widget = response.data.get('dates_widget')
+        self.assertTrue(dates_widget)
+        date_blocks = dates_widget.get('course_date_blocks')
+        self.assertEqual(len(date_blocks), 0)
 
     @override_experiment_waffle_flag(COURSE_HOME_MICROFRONTEND, active=True)
     @override_waffle_flag(COURSE_HOME_MICROFRONTEND_OUTLINE_TAB, active=True)
diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py
index 15d2298f031a4876a96d491245602291b6e8ce29..93c111b0a559a3ba453772e7c9981bb0b896e92c 100644
--- a/lms/djangoapps/course_home_api/outline/v1/views.py
+++ b/lms/djangoapps/course_home_api/outline/v1/views.py
@@ -134,12 +134,10 @@ class OutlineTabView(RetrieveAPIView):
     **Returns**
 
         * 200 on success with above fields.
-        * 403 if the user is not authenticated.
         * 404 if the course is not available or cannot be seen.
 
     """
 
-    permission_classes = (IsAuthenticated,)
     serializer_class = OutlineTabSerializer
 
     def get(self, request, *args, **kwargs):
@@ -169,32 +167,6 @@ class OutlineTabView(RetrieveAPIView):
         allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key)
         allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC
         allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE
-        is_enrolled = enrollment and enrollment.is_active
-        is_staff = bool(has_access(request.user, 'staff', course_key))
-        show_enrolled = is_enrolled or is_staff
-
-        show_handouts = show_enrolled or allow_public
-        handouts_html = get_course_info_section(request, request.user, course, 'handouts') if show_handouts else ''
-
-        offer_data = show_enrolled and generate_offer_data(request.user, course_overview)
-        access_expiration = show_enrolled and get_access_expiration_data(request.user, course_overview)
-
-        welcome_message_html = show_enrolled and get_current_update_for_user(request, course)
-
-        enroll_alert = {
-            'can_enroll': True,
-            'extra_text': None,
-        }
-        if not show_enrolled:
-            if CourseMode.is_masters_only(course_key):
-                enroll_alert['can_enroll'] = False
-                enroll_alert['extra_text'] = _('Please contact your degree administrator or '
-                                               'edX Support if you have questions.')
-            elif course.invitation_only:
-                enroll_alert['can_enroll'] = False
-
-        course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
-        date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1)
 
         # User locale settings
         user_timezone_locale = user_timezone_locale_prefs(request)
@@ -204,16 +176,62 @@ class OutlineTabView(RetrieveAPIView):
         if course_home_mfe_dates_tab_is_active(course.id):
             dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates')
 
+        # Set all of the defaults
+        access_expiration = None
         course_blocks = None
-        if show_enrolled or allow_public or allow_public_outline:
-            outline_user = request.user if show_enrolled else None
-            course_blocks = get_course_outline_block_tree(request, course_key_string, outline_user)
-
+        course_goals = {
+            'goal_options': [],
+            'selected_goal': None
+        }
+        course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
+        dates_widget = {
+            'course_date_blocks': [],
+            'dates_tab_link': dates_tab_link,
+            'user_timezone': user_timezone,
+        }
+        enroll_alert = {
+            'can_enroll': True,
+            'extra_text': None,
+        }
+        handouts_html = None
+        offer_data = None
         resume_course = {
             'has_visited_course': False,
             'url': None,
         }
+        welcome_message_html = None
+
+        is_enrolled = enrollment and enrollment.is_active
+        is_staff = bool(has_access(request.user, 'staff', course_key))
+        show_enrolled = is_enrolled or is_staff
         if show_enrolled:
+            course_blocks = get_course_outline_block_tree(request, course_key_string, request.user)
+            date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1)
+            dates_widget['course_date_blocks'] = [block for block in date_blocks if not isinstance(block, TodaysDate)]
+
+            handouts_html = get_course_info_section(request, request.user, course, 'handouts')
+            welcome_message_html = get_current_update_for_user(request, course)
+
+            offer_data = generate_offer_data(request.user, course_overview)
+            access_expiration = get_access_expiration_data(request.user, course_overview)
+
+            # Only show the set course goal message for enrolled, unverified
+            # users in a course that allows for verified statuses.
+            is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
+            if not is_already_verified and has_course_goal_permission(request, course_key_string,
+                                                                      {'is_enrolled': is_enrolled}):
+                course_goals = {
+                    'goal_options': valid_course_goals_ordered(include_unsure=True),
+                    'selected_goal': None
+                }
+
+                selected_goal = get_course_goal(request.user, course_key)
+                if selected_goal:
+                    course_goals['selected_goal'] = {
+                        'key': selected_goal.goal_key,
+                        'text': get_course_goal_text(selected_goal.goal_key),
+                    }
+
             try:
                 resume_block = get_key_to_last_completed_block(request.user, course.id)
                 resume_course['has_visited_course'] = True
@@ -224,47 +242,31 @@ class OutlineTabView(RetrieveAPIView):
                 'location': str(resume_block)
             })
             resume_course['url'] = request.build_absolute_uri(resume_path)
+        elif allow_public_outline or allow_public:
+            course_blocks = get_course_outline_block_tree(request, course_key_string, None)
+            if allow_public:
+                handouts_html = get_course_info_section(request, request.user, course, 'handouts')
 
-        dates_widget = {
-            'course_date_blocks': [block for block in date_blocks if not isinstance(block, TodaysDate)],
-            'dates_tab_link': dates_tab_link,
-            'user_timezone': user_timezone,
-        }
-
-        # Only show the set course goal message for enrolled, unverified
-        # users in a course that allows for verified statuses.
-        is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
-        if (not is_already_verified and
-                has_course_goal_permission(request, course_key_string, {'is_enrolled': is_enrolled})):
-            course_goals = {
-                'goal_options': valid_course_goals_ordered(include_unsure=True),
-                'selected_goal': None
-            }
-
-            selected_goal = get_course_goal(request.user, course_key)
-            if selected_goal:
-                course_goals['selected_goal'] = {
-                    'key': selected_goal.goal_key,
-                    'text': get_course_goal_text(selected_goal.goal_key),
-                }
-        else:
-            course_goals = {
-                'goal_options': [],
-                'selected_goal': None
-            }
+        if not show_enrolled:
+            if CourseMode.is_masters_only(course_key):
+                enroll_alert['can_enroll'] = False
+                enroll_alert['extra_text'] = _('Please contact your degree administrator or '
+                                               'edX Support if you have questions.')
+            elif course.invitation_only:
+                enroll_alert['can_enroll'] = False
 
         data = {
-            'access_expiration': access_expiration or None,
+            'access_expiration': access_expiration,
             'course_blocks': course_blocks,
             'course_goals': course_goals,
             'course_tools': course_tools,
             'dates_widget': dates_widget,
             'enroll_alert': enroll_alert,
-            'handouts_html': handouts_html or None,
+            'handouts_html': handouts_html,
             'has_ended': course.has_ended(),
-            'offer': offer_data or None,
+            'offer': offer_data,
             'resume_course': resume_course,
-            'welcome_message_html': welcome_message_html or None,
+            'welcome_message_html': welcome_message_html,
         }
         context = self.get_serializer_context()
         context['course_overview'] = course_overview
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index a63a479e50e88d7c40566b1f622f84ea4632bbf5..19eef9f3dfd3da2a7a5ba9cdcdb1bac79def4d60 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -525,6 +525,8 @@ def get_course_assignments(course_key, user, include_access=False):
     Each returned object is a namedtuple with fields: title, url, date, contains_gated_content, complete, past_due,
     assignment_type
     """
+    if not user.id:
+        return []
     store = modulestore()
     course_usage_key = store.make_course_usage_key(course_key)
     block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True)
diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py
index 582438de5018889f79b28fd428ff7592df07150d..fe7899187c1c16aba6e14098801ea2fd8b9d34b8 100644
--- a/lms/djangoapps/courseware/views/views.py
+++ b/lms/djangoapps/courseware/views/views.py
@@ -55,7 +55,8 @@ from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
 from lms.djangoapps.certificates import api as certs_api
 from lms.djangoapps.certificates.models import CertificateStatuses
 from lms.djangoapps.commerce.utils import EcommerceService
-from lms.djangoapps.course_home_api.utils import is_request_from_learning_mfe
+from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active
+from lms.djangoapps.course_home_api.utils import get_microfrontend_url, is_request_from_learning_mfe
 from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role
 from lms.djangoapps.courseware.access_utils import check_course_open_for_learner, check_public_access
 from lms.djangoapps.courseware.courses import (
@@ -643,7 +644,8 @@ class CourseTabView(EdxFragmentView):
                             register_label=_("register"),
                             current_url=urlquote_plus(request.path),
                         ),
-                    )
+                    ),
+                    once_only=True
                 )
             else:
                 PageLevelMessages.register_warning_message(
@@ -1010,6 +1012,9 @@ def dates(request, course_id):
     from lms.urls import COURSE_DATES_NAME, RESET_COURSE_DEADLINES_NAME
 
     course_key = CourseKey.from_string(course_id)
+    if course_home_mfe_dates_tab_is_active(course_key) and not request.user.is_staff:
+        microfrontend_url = get_microfrontend_url(course_key=course_key, view_name=COURSE_DATES_NAME)
+        raise Redirect(microfrontend_url)
 
     # Enable NR tracing for this view based on course
     monitoring_utils.set_custom_attribute('course_id', text_type(course_key))
@@ -1617,7 +1622,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
     Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
     The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
     """
-    from lms.urls import COURSE_DATES_NAME, RESET_COURSE_DEADLINES_NAME
+    from lms.urls import RESET_COURSE_DEADLINES_NAME
     from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME
 
     usage_key = UsageKey.from_string(usage_key_string)
@@ -1626,7 +1631,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
     course_key = usage_key.course_key
 
     requested_view = request.GET.get('view', 'student_view')
-    if requested_view != 'student_view':
+    if requested_view != 'student_view' and requested_view != 'public_view':
         return HttpResponseBadRequest(
             u"Rendering of the xblock view '{}' is not supported.".format(bleach.clean(requested_view, strip=True))
         )
@@ -1659,7 +1664,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
         missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
 
         context = {
-            'fragment': block.render('student_view', context=student_view_context),
+            'fragment': block.render(requested_view, context=student_view_context),
             'course': course,
             'disable_accordion': True,
             'allow_iframing': True,
diff --git a/lms/djangoapps/experiments/flags.py b/lms/djangoapps/experiments/flags.py
index 4d9fd09c67cef8234fe0ebde21a757398cfa1c8e..9af33fd93fcdb4fde73cfdff76dda8ec76784ece 100644
--- a/lms/djangoapps/experiments/flags.py
+++ b/lms/djangoapps/experiments/flags.py
@@ -189,16 +189,14 @@ class ExperimentWaffleFlag(CourseWaffleFlag):
         if not request:
             return 0
 
-        if not hasattr(request, 'user') or not request.user.id:
-            # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users
-            return 0
+        if hasattr(request, 'user'):
+            user = get_specific_masquerading_user(request.user, course_key)
 
-        user = get_specific_masquerading_user(request.user, course_key)
-        if user is None:
-            user = request.user
-            masquerading_as_specific_student = False
-        else:
-            masquerading_as_specific_student = True
+            if user is None:
+                user = request.user
+                masquerading_as_specific_student = False
+            else:
+                masquerading_as_specific_student = True
 
         # If a course key is passed in, include it in the experiment name
         # in order to separate caches and analytics calls per course-run.
@@ -238,14 +236,15 @@ class ExperimentWaffleFlag(CourseWaffleFlag):
                 break
         else:
             bucket = stable_bucketing_hash_group(
-                bucketing_group_name, self.num_buckets, user.username
+                bucketing_group_name, self.num_buckets, user
             )
 
         session_key = 'tracked.{}'.format(experiment_name)
+        anonymous = not hasattr(request, 'user') or not request.user.id
         if (
                 track and hasattr(request, 'session') and
                 session_key not in request.session and
-                not masquerading_as_specific_student
+                not masquerading_as_specific_student and not anonymous
         ):
             segment.track(
                 user_id=user.id,
diff --git a/lms/djangoapps/experiments/stable_bucketing.py b/lms/djangoapps/experiments/stable_bucketing.py
index b6709fd9639b10fd864e37fa475bbd20ce3bf675..bc3039ac32cf7ad178c1da85122513cacef606ed 100644
--- a/lms/djangoapps/experiments/stable_bucketing.py
+++ b/lms/djangoapps/experiments/stable_bucketing.py
@@ -12,7 +12,7 @@ import hashlib
 import re
 
 
-def stable_bucketing_hash_group(group_name, group_count, username):
+def stable_bucketing_hash_group(group_name, group_count, user):
     """
     Return the bucket that a user should be in for a given stable bucketing assignment.
 
@@ -22,11 +22,14 @@ def stable_bucketing_hash_group(group_name, group_count, username):
     Arguments:
         group_name: The name of the grouping/experiment.
         group_count: How many groups to bucket users into.
-        username: The username of the user being bucketed.
+        user: The user being bucketed.
     """
+    # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users
+    if not user or not user.id:
+        return 0
     hasher = hashlib.md5()
     hasher.update(group_name.encode('utf-8'))
-    hasher.update(username.encode('utf-8'))
+    hasher.update(user.username.encode('utf-8'))
     hash_str = hasher.hexdigest()
 
     return int(re.sub('[8-9a-f]', '1', re.sub('[0-7]', '0', hash_str)), 2) % group_count
diff --git a/lms/djangoapps/experiments/views_custom.py b/lms/djangoapps/experiments/views_custom.py
index a1e901cd2486c6fbc19e4bf20f1a1ce32ab7adfd..0cbcfe2eb8a66198576fe59e7cde5ebfd1944d08 100644
--- a/lms/djangoapps/experiments/views_custom.py
+++ b/lms/djangoapps/experiments/views_custom.py
@@ -138,7 +138,7 @@ class Rev934(DeveloperErrorViewMixin, APIView):
         upgrade_price = six.text_type(get_cosmetic_verified_display_price(course))
         could_upsell = bool(user_upsell and basket_url)
 
-        bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2, user.username)
+        bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2, user)
 
         if could_upsell and hasattr(request, 'session') and MOBILE_UPSELL_EXPERIMENT not in request.session:
             properties = {
diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py
index 5e12e1f9c61059c1479623336814c0f37d5af5bc..46c4f32ce95e24e5b5954fd19d6ce7304318ae5c 100644
--- a/openedx/core/djangoapps/courseware_api/views.py
+++ b/openedx/core/djangoapps/courseware_api/views.py
@@ -47,6 +47,7 @@ from common.djangoapps.student.models import (
 )
 from xmodule.modulestore.django import modulestore
 from xmodule.modulestore.search import path_to_location
+from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
 
 from .serializers import CourseInfoSerializer
 from .utils import serialize_upgrade_info
@@ -483,7 +484,12 @@ class SequenceMetadata(DeveloperErrorViewMixin, APIView):
             str(usage_key.course_key),
             str(usage_key),
             disable_staff_debug_info=True)
-        return Response(json.loads(sequence.handle_ajax('metadata', None)))
+
+        view = STUDENT_VIEW
+        if request.user.is_anonymous:
+            view = PUBLIC_VIEW
+
+        return Response(json.loads(sequence.handle_ajax('metadata', None, view=view)))
 
 
 class Resume(DeveloperErrorViewMixin, APIView):
diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py
index 78e439742bc15a988d99d3e9d22785a89d6a383f..b2e8ae0b7440b3b303d7eaf0c10f3d765bacbee2 100644
--- a/openedx/features/course_experience/views/course_dates.py
+++ b/openedx/features/course_experience/views/course_dates.py
@@ -12,7 +12,6 @@ from django.utils.translation import get_language_bidi
 from opaque_keys.edx.keys import CourseKey
 from web_fragments.fragment import Fragment
 
-from lms.djangoapps.courseware.access import has_access
 from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
 from lms.djangoapps.courseware.tabs import DatesTab
 from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active
diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py
index be35a428c242734517b599697b4e53263d8b106e..80d6686b475270f1879cd068a18cefcbed234373 100644
--- a/openedx/features/course_experience/views/course_home.py
+++ b/openedx/features/course_experience/views/course_home.py
@@ -14,16 +14,17 @@ from django.views.decorators.csrf import ensure_csrf_cookie
 from opaque_keys.edx.keys import CourseKey
 from web_fragments.fragment import Fragment
 
+from lms.djangoapps.course_home_api.toggles import course_home_mfe_outline_tab_is_active
+from lms.djangoapps.course_home_api.utils import get_microfrontend_url
 from lms.djangoapps.courseware.access import has_access
 from lms.djangoapps.courseware.courses import can_self_enroll_in_course, get_course_info_section, get_course_with_access
-from lms.djangoapps.commerce.utils import EcommerceService
 from lms.djangoapps.course_goals.api import (
     get_course_goal,
     get_course_goal_options,
     get_goal_api_url,
     has_course_goal_permission
 )
-from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
+from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect
 from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link
 from lms.djangoapps.courseware.views.views import CourseTabView
 from lms.djangoapps.courseware.toggles import COURSEWARE_PROCTORING_IMPROVEMENTS
@@ -70,6 +71,9 @@ class CourseHomeView(CourseTabView):
 
     def render_to_fragment(self, request, course=None, tab=None, **kwargs):
         course_id = six.text_type(course.id)
+        if course_home_mfe_outline_tab_is_active(course.id) and not request.user.is_staff:
+            microfrontend_url = get_microfrontend_url(course_key=course_id, view_name="home")
+            raise Redirect(microfrontend_url)
         home_fragment_view = CourseHomeFragmentView()
         return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs)
 
diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py
index d0efa0ac49a329c322dc0649f0ff01014b66e54c..f979e46eeb8a93b4252bd234a5e81aa52658d372 100644
--- a/openedx/features/discounts/applicability.py
+++ b/openedx/features/discounts/applicability.py
@@ -150,7 +150,7 @@ def _is_in_holdback_and_bucket(user):
         return False
 
     # Holdback is 10%
-    bucket = stable_bucketing_hash_group(DISCOUNT_APPLICABILITY_HOLDBACK, 10, user.username)
+    bucket = stable_bucketing_hash_group(DISCOUNT_APPLICABILITY_HOLDBACK, 10, user)
 
     request = get_current_request()
     if hasattr(request, 'session') and DISCOUNT_APPLICABILITY_HOLDBACK not in request.session: