diff --git a/README.rst b/README.rst
index bd2fbf8181b9d65dbb59da724825891b1628cd6d..ee17c403c5e23bd2e149795649fdcfab9fbbae27 100644
--- a/README.rst
+++ b/README.rst
@@ -139,9 +139,11 @@ Instructions:
 
 .. admonition:: Testing using ``ngrok``
 
-    When launching LTI 1.3 requests through ``ngrok``, make sure you set ``DCS_SESSION_COOKIE_SAMESITE = 'None'`` in your
-    ``devstack.py`` (located in /edx/app/edxapp/edx-platform/(lms|cms)/envs``) when doing LTI 1.3 launches in the
-    devstack through ngrok. Do not forget to restart your services after updating the ``.py`` files.
+    When launching LTI 1.3 requests through ``ngrok``, make sure your LMS is serving session cookies marked as
+    ``Secure`` and with the ``SameSite`` attribute set to ``None``. You can do this by changing ``SESSION_COOKIE_SECURE: true``
+    and ``DCS_SESSION_COOKIE_SAMESITE: None`` in your ``lms.yml`` configuration files. Note that this will break logins
+    for locally accessed URLs in the devstack.
+
 
 Custom LTI Parameters
 =====================
@@ -367,6 +369,17 @@ Changelog
 
 Please See the [releases tab](https://github.com/edx/xblock-lti-consumer/releases) for the complete changelog.
 
+3.4.6 - 2022-03-31
+------------------
+
+* Fix rendering of `lti_1p3_launch_error.html` and `lti_1p3_permission_error.html` templates
+
+3.4.5 - 2022-03-16
+------------------
+
+* Fix LTI Deep Linking return endpoint permission checking method by replacing the old one with the proper
+  Studio API call.
+
 3.4.4 - 2022-03-03
 ------------------
 
diff --git a/lti_consumer/__init__.py b/lti_consumer/__init__.py
index ed92eb5d89f723f051797376ec443aaf8e65e40b..e78f3aeb05935b044c3e9b2e2b14df80c3b545aa 100644
--- a/lti_consumer/__init__.py
+++ b/lti_consumer/__init__.py
@@ -4,4 +4,4 @@ Runtime will load the XBlock class from here.
 from .apps import LTIConsumerApp
 from .lti_xblock import LtiConsumerXBlock
 
-__version__ = '3.4.4'
+__version__ = '3.4.6'
diff --git a/lti_consumer/lti_xblock.py b/lti_consumer/lti_xblock.py
index 21b3e939ef5bed1e3a2d465a9461d2fa5973f04a..336df6e7abaddbdc3775cce85aecec5afe460f6b 100644
--- a/lti_consumer/lti_xblock.py
+++ b/lti_consumer/lti_xblock.py
@@ -685,7 +685,8 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
         """
         Get system user role and convert it to LTI role.
         """
-        return ROLE_MAP.get(self.runtime.get_user_role(), 'Student')
+        role = self.runtime.service(self, 'user').get_current_user().opt_attrs.get('edx-platform.user_role', 'student')
+        return ROLE_MAP.get(role, 'Student')
 
     @property
     def course(self):
@@ -723,11 +724,15 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
     def user_id(self):
         """
         Returns the opaque anonymous_student_id for the current user.
+        This defaults to 'student' when testing in studio.
+        It will return the proper anonymous ID in the LMS.
         """
-        user_id = self.runtime.anonymous_student_id
+        user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(
+            'edx-platform.anonymous_user_id', None)
+
         if user_id is None:
             raise LtiError(self.ugettext("Could not get user id for current request"))
-        return str(urllib.parse.quote(user_id))
+        return str(user_id)
 
     def get_icon_class(self):
         """ Returns the icon class """
@@ -912,22 +917,20 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
         """
         Extract and return real user data from the runtime
         """
+        user = self.runtime.service(self, 'user').get_current_user()
         user_data = {
             'user_email': None,
             'user_username': None,
             'user_language': None,
         }
 
-        if callable(self.runtime.get_real_user):
-            real_user_object = self.runtime.get_real_user(self.runtime.anonymous_student_id)
-            user_data['user_email'] = getattr(real_user_object, "email", "")
-            user_data['user_username'] = getattr(real_user_object, "username", "")
-            user_preferences = getattr(real_user_object, "preferences", None)
+        try:
+            user_data['user_email'] = user.emails[0]
+        except IndexError:
+            user_data['user_email'] = None
 
-            if user_preferences is not None:
-                language_preference = user_preferences.filter(key='pref-lang')
-                if len(language_preference) == 1:
-                    user_data['user_language'] = language_preference[0].value
+        user_data['user_username'] = user.opt_attrs.get("edx-platform.username", None)
+        user_data['user_language'] = user.opt_attrs.get("edx-platform.user_preferences", {}).get("pref-lang", None)
 
         return user_data
 
@@ -1163,7 +1166,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
                 str(self.location),  # pylint: disable=no-member
                 exc,
             )
-            template = loader.render_mako_template('/templates/html/lti_1p3_launch_error.html', context)
+            template = loader.render_django_template('/templates/html/lti_1p3_launch_error.html', context)
             return Response(template, status=400, content_type='text/html')
         except AssertionError as exc:
             log.warning(
@@ -1172,7 +1175,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
                 self.external_user_id,
                 exc,
             )
-            template = loader.render_mako_template('/templates/html/lti_1p3_permission_error.html', context)
+            template = loader.render_django_template('/templates/html/lti_1p3_permission_error.html', context)
             return Response(template, status=403, content_type='text/html')
 
     @XBlock.handler
diff --git a/lti_consumer/plugin/compat.py b/lti_consumer/plugin/compat.py
index d64a5aeab8cfd479cc357c57ff7391a90dd323a9..e2acf520a25bc4c6550ee3c547d472d504f93cde 100644
--- a/lti_consumer/plugin/compat.py
+++ b/lti_consumer/plugin/compat.py
@@ -113,6 +113,18 @@ def user_has_access(*args, **kwargs):
     return has_access(*args, **kwargs)
 
 
+def user_has_studio_write_access(*args, **kwargs):
+    """
+    Import and run `has_studio_write_access` from common modules.
+
+    Used to check if someone saving deep linking content has the
+    correct write permissions for a given.
+    """
+    # pylint: disable=import-error,import-outside-toplevel
+    from common.djangoapps.student.auth import has_studio_write_access
+    return has_studio_write_access(*args, **kwargs)
+
+
 def get_course_by_id(course_key):
     """
     Import and run `get_course_by_id` from LMS
diff --git a/lti_consumer/plugin/views.py b/lti_consumer/plugin/views.py
index 4cc4bd8777c23c3e204699a69c6ca7223219de2d..abe52d27ecf61b5783db58c1cec9767a22159c4c 100644
--- a/lti_consumer/plugin/views.py
+++ b/lti_consumer/plugin/views.py
@@ -63,13 +63,6 @@ from lti_consumer.utils import _
 log = logging.getLogger(__name__)
 
 
-def user_has_staff_access(user, course_key):
-    """
-    Check if an user has write permissions to a given course.
-    """
-    return compat.user_has_access(user, "staff", course_key)
-
-
 def has_block_access(user, block, course_key):
     """
     Checks if a user has access to given xblock.
@@ -183,26 +176,31 @@ def access_token_endpoint(request, usage_id=None):
 @require_http_methods(["POST"])
 def deep_linking_response_endpoint(request, lti_config_id=None):
     """
-    Deep Linking response endpoint where tool can send back
+    Deep Linking response endpoint where tool can send back Deep Linking
+    content selected by instructions in the tool's UI.
+
+    For this feature to work, the LMS session cookies need to be Secure
+    and have the `SameSite` attribute set to `None`, otherwise we won't
+    be able to check user permissions.
     """
     try:
         # Retrieve LTI configuration
         lti_config = LtiConfiguration.objects.get(id=lti_config_id)
 
-        # First, check if the user has sufficient permissions to
-        # save LTI Deep Linking content through the student.auth API.
-        course_key = lti_config.location.course_key
-        if not user_has_staff_access(request.user, course_key):
-            raise PermissionDenied()
-
         # Get LTI consumer
         lti_consumer = lti_config.get_lti_consumer()
 
-        # Retrieve Deep Linking return message and validate parameters
+        # Validate Deep Linking return message and return decoded message
         content_items = lti_consumer.check_and_decode_deep_linking_token(
             request.POST.get("JWT")
         )
 
+        # Check if the user has sufficient permissions to
+        # save LTI Deep Linking content through the student.auth API.
+        course_key = lti_config.location.course_key
+        if not compat.user_has_studio_write_access(request.user, course_key):
+            raise PermissionDenied()
+
         # On a transaction, clear older DeepLinking selections, then
         # verify and save each content item passed from the tool.
         with transaction.atomic():
diff --git a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py
index 58f77660bd02c159a1fb988b8138ed127217089d..77505f9a28c2fe20d171c97c75fc1839dd26c433 100644
--- a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py
+++ b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py
@@ -89,10 +89,17 @@ class LtiDeepLinkingResponseEndpointTestCase(LtiDeepLinkingTestCase):
         super().setUp()
 
         # Patch method that calls platform core to ask for user permissions
-        studio_access_patcher = patch('lti_consumer.plugin.views.user_has_staff_access')
-        self.addCleanup(studio_access_patcher.stop)
-        self._mock_has_studio_write_acess = studio_access_patcher.start()
-        self._mock_has_studio_write_acess.return_value = True
+        compat_mock = patch("lti_consumer.signals.compat")
+        self.addCleanup(compat_mock.stop)
+        self._compat_mock = compat_mock.start()
+        self._compat_mock.user_has_studio_write_access.return_value = True
+
+        has_studio_write_acess_patcher = patch(
+            'lti_consumer.plugin.views.compat.user_has_studio_write_access',
+            return_value=True
+        )
+        self.addCleanup(has_studio_write_acess_patcher.stop)
+        self._mock_has_studio_write_acess = has_studio_write_acess_patcher.start()
 
         # Deep Linking response endpoint
         self.url = '/lti_consumer/v1/lti/{}/lti-dl/response'.format(self.lti_config.id)
diff --git a/lti_consumer/tests/unit/test_lti_xblock.py b/lti_consumer/tests/unit/test_lti_xblock.py
index 9a3e6a0201cdaecedb12a4afdbe75c3ce6ef4f53..fbada0498baae8b66b83e515d4a65f84169aea41 100644
--- a/lti_consumer/tests/unit/test_lti_xblock.py
+++ b/lti_consumer/tests/unit/test_lti_xblock.py
@@ -5,7 +5,7 @@ Unit tests for LtiConsumerXBlock
 import json
 import logging
 from datetime import timedelta
-from unittest.mock import Mock, NonCallableMock, PropertyMock, patch
+from unittest.mock import Mock, PropertyMock, patch
 
 import ddt
 from Cryptodome.PublicKey import RSA
@@ -132,16 +132,29 @@ class TestProperties(TestLtiConsumerXBlock):
         """
         Test `role` returns the correct LTI role string
         """
-        self.xblock.runtime.get_user_role.return_value = 'student'
+        fake_user = Mock()
+        fake_user.opt_attrs = {
+            'edx-platform.user_role': 'student'
+        }
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
         self.assertEqual(self.xblock.role, 'Student')
 
-        self.xblock.runtime.get_user_role.return_value = 'guest'
+        fake_user.opt_attrs = {
+            'edx-platform.user_role': 'guest'
+        }
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
         self.assertEqual(self.xblock.role, 'Student')
 
-        self.xblock.runtime.get_user_role.return_value = 'staff'
+        fake_user.opt_attrs = {
+            'edx-platform.user_role': 'staff'
+        }
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
         self.assertEqual(self.xblock.role, 'Administrator')
 
-        self.xblock.runtime.get_user_role.return_value = 'instructor'
+        fake_user.opt_attrs = {
+            'edx-platform.user_role': 'instructor'
+        }
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
         self.assertEqual(self.xblock.role, 'Instructor')
 
     def test_course(self):
@@ -218,21 +231,25 @@ class TestProperties(TestLtiConsumerXBlock):
         """
         Test `user_id` returns the user_id string
         """
-        self.xblock.runtime.anonymous_student_id = FAKE_USER_ID
-        self.assertEqual(self.xblock.user_id, FAKE_USER_ID)
+        fake_user = Mock()
+        fake_user.opt_attrs = {
+            'edx-platform.anonymous_user_id': FAKE_USER_ID
+        }
 
-    def test_user_id_url_encoded(self):
-        """
-        Test `user_id` url encodes the user id
-        """
-        self.xblock.runtime.anonymous_student_id = 'user_id?&. '
-        self.assertEqual(self.xblock.user_id, 'user_id%3F%26.%20')
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
+        self.assertEqual(self.xblock.user_id, FAKE_USER_ID)
 
     def test_user_id_none(self):
         """
         Test `user_id` raises LtiError when the user id cannot be returned
         """
-        self.xblock.runtime.anonymous_student_id = None
+        fake_user = Mock()
+        fake_user.opt_attrs = {
+            'edx-platform.anonymous_user_id': None
+        }
+
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
+
         with self.assertRaises(LtiError):
             __ = self.xblock.user_id
 
@@ -490,48 +507,47 @@ class TestExtractRealUserData(TestLtiConsumerXBlock):
     Unit tests for LtiConsumerXBlock.extract_real_user_data()
     """
 
-    def test_get_real_user_not_callable(self):
-        """
-        Test user_email, user_username, and user_language not available
-        """
-        self.xblock.runtime.get_real_user = NonCallableMock()
-
-        real_user_data = self.xblock.extract_real_user_data()
-        self.assertIsNone(real_user_data['user_email'])
-        self.assertIsNone(real_user_data['user_username'])
-        self.assertIsNone(real_user_data['user_language'])
-
     def test_get_real_user_callable(self):
         """
         Test user_email, and user_username available, but not user_language
+        See also documentation of new user service:
+        https://github.com/openedx/XBlock/blob/master/xblock/reference/user_service.py
         """
         fake_user = Mock()
-        fake_user.email = 'abc@example.com'
-        fake_user.username = 'fake'
-        fake_user.preferences = None
+        fake_user_email = 'abc@example.com'
+        fake_user.emails = [fake_user_email]
+        fake_username = 'fake'
+        fake_user.opt_attrs = {
+            "edx-platform.username": fake_username
+        }
 
-        self.xblock.runtime.get_real_user = Mock(return_value=fake_user)
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
 
         real_user_data = self.xblock.extract_real_user_data()
-        self.assertEqual(real_user_data['user_email'], fake_user.email)
-        self.assertEqual(real_user_data['user_username'], fake_user.username)
+        self.assertEqual(real_user_data['user_email'], fake_user_email)
+        self.assertEqual(real_user_data['user_username'], fake_username)
         self.assertIsNone(real_user_data['user_language'])
 
     def test_get_real_user_callable_with_language_preference(self):
         """
         Test user_language available
+        See also documentation of new user service:
+        https://github.com/openedx/XBlock/blob/master/xblock/reference/user_service.py
         """
         fake_user = Mock()
-        fake_user.email = 'abc@example.com'
-        fake_user.username = 'fake'
-        mock_language_pref = Mock()
-        mock_language_pref.value = PropertyMock(return_value='en')
-        fake_user.preferences.filter = Mock(return_value=[mock_language_pref])
+        fake_user.emails = ['abc@example.com']
+        fake_user.full_name = 'fake'
+        pref_language = "en"
+        fake_user.opt_attrs = {
+            "edx-platform.user_preferences": {
+                "pref-lang": "en"
+            }
+        }
 
-        self.xblock.runtime.get_real_user = Mock(return_value=fake_user)
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
 
         real_user_data = self.xblock.extract_real_user_data()
-        self.assertEqual(real_user_data['user_language'], mock_language_pref.value)
+        self.assertEqual(real_user_data['user_language'], pref_language)
 
 
 class TestStudentView(TestLtiConsumerXBlock):
@@ -630,7 +646,16 @@ class TestLtiLaunchHandler(TestLtiConsumerXBlock):
         self.xblock._get_lti_consumer = Mock(return_value=self.mock_lti_consumer)  # pylint: disable=protected-access
         self.xblock.due = timezone.now()
         self.xblock.graceperiod = timedelta(days=1)
-        self.xblock.runtime.get_real_user = Mock(return_value=None)
+
+        fake_user = Mock()
+        fake_user_email = 'abc@example.com'
+        fake_user.emails = [fake_user_email]
+        fake_username = 'fake'
+        fake_user.opt_attrs = {
+            "edx-platform.username": fake_username
+        }
+
+        self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
 
     @patch('lti_consumer.lti_xblock.LtiConsumerXBlock.course')
     @patch('lti_consumer.lti_xblock.LtiConsumerXBlock.user_id', PropertyMock(return_value=FAKE_USER_ID))
@@ -1268,6 +1293,7 @@ class TestLtiConsumer1p3XBlock(TestCase):
 
         response_body = response.body.decode('utf-8')
         self.assertIn("There was an error while launching the LTI 1.3 tool.", response_body)
+        self.assertNotIn("% trans", response_body)
 
     def test_launch_callback_endpoint_when_using_lti_1p1(self):
         """
@@ -1372,6 +1398,9 @@ class TestLtiConsumer1p3XBlock(TestCase):
 
         # Check response
         self.assertEqual(response.status_code, 403)
+        response_body = response.body.decode('utf-8')
+        self.assertIn("Students don't have permissions to perform", response_body)
+        self.assertNotIn("% trans", response_body)
 
     @patch('lti_consumer.api.get_deep_linking_data')
     def test_callback_endpoint_dl_content_launch(self, mock_dl_data):