Skip to content

Commit e52699f

Browse files
feat: handle 500 errors that occur when user is unauthenticated during LTI 1.1 launch
In the LTI 1.1 launch handler, we set the user context, including the user_id. We do this by calling to the LMS's DjangoXBlockUserService to get information about the user. Sometimes, the user is unauthenticated. Sometimes, this is because the user is a web crawler. Other times, the user is a real user, but we do not know why the user is unauthenticated. We have some theories, but we have been unable to confirm them. Regardless, we should not surface a 500 error to the user. This commit adds handling for the LtiError that is raised when a user is unauthenticated during an LTI 1.1 launch. It catches the LtiError and renders an error page. The error page that was used for LTI 1.3 launches, formerly named "lti_1p3_launch_error.html", has been renamed to "lti_launch_error.html" to reflect the fact that it is used for both LTI 1.1 and 1.3 launches. It was modified to remove the reference to the version of LTI used by the XBlock; these details are unnecessary for a learner, and removing them allows us to reuse a single template for both LTI versions.
1 parent be8462b commit e52699f

6 files changed

Lines changed: 98 additions & 20 deletions

File tree

lti_consumer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from .apps import LTIConsumerApp
55
from .lti_xblock import LtiConsumerXBlock
66

7-
__version__ = '4.4.0'
7+
__version__ = '4.5.0'

lti_consumer/lti_xblock.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,11 @@ def role(self):
745745
"""
746746
Get system user role and convert it to LTI role.
747747
"""
748-
role = self.runtime.service(self, 'user').get_current_user().opt_attrs.get('edx-platform.user_role', 'student')
748+
user = self.runtime.service(self, 'user').get_current_user()
749+
if not user.opt_attrs["edx-platform.is_authenticated"]:
750+
raise LtiError(self.ugettext("Could not get user data for current request"))
751+
752+
role = user.opt_attrs.get('edx-platform.user_role', 'student')
749753
return ROLE_MAP.get(role, 'Student,Learner')
750754

751755
@property
@@ -978,6 +982,10 @@ def extract_real_user_data(self):
978982
Extract and return real user data from the runtime
979983
"""
980984
user = self.runtime.service(self, 'user').get_current_user()
985+
986+
if not user.opt_attrs["edx-platform.is_authenticated"]:
987+
raise LtiError(self.ugettext("Could not get user data for current request"))
988+
981989
user_data = {
982990
'user_email': None,
983991
'user_username': None,
@@ -1080,10 +1088,21 @@ def lti_launch_handler(self, request, suffix=''): # pylint: disable=unused-argu
10801088
Returns:
10811089
webob.response: HTML LTI launch form
10821090
"""
1083-
real_user_data = self.extract_real_user_data()
1084-
10851091
lti_consumer = self._get_lti_consumer()
10861092

1093+
# Occassionally, users try to do an LTI launch while they are unauthenticated. It is not known why this occurs.
1094+
# Sometimes, it is due to a web crawlers; other times, it is due to actual users of the platform. Regardless,
1095+
# return a 400 response with an appropriate error template.
1096+
try:
1097+
real_user_data = self.extract_real_user_data()
1098+
user_id = self.user_id
1099+
role = self.role
1100+
result_sourcedid = self.lis_result_sourcedid
1101+
except LtiError:
1102+
loader = ResourceLoader(__name__)
1103+
template = loader.render_django_template('/templates/html/lti_launch_error.html')
1104+
return Response(template, status=400, content_type='text/html')
1105+
10871106
username = None
10881107
email = None
10891108
if self.ask_to_send_username and real_user_data['user_username']:
@@ -1092,12 +1111,13 @@ def lti_launch_handler(self, request, suffix=''): # pylint: disable=unused-argu
10921111
email = real_user_data['user_email']
10931112

10941113
lti_consumer.set_user_data(
1095-
self.user_id,
1096-
self.role,
1097-
result_sourcedid=self.lis_result_sourcedid,
1114+
user_id,
1115+
role,
1116+
result_sourcedid=result_sourcedid,
10981117
person_sourcedid=username,
10991118
person_contact_email_primary=email
11001119
)
1120+
11011121
lti_consumer.set_context_data(
11021122
self.context_id,
11031123
self.course.display_name_with_default,

lti_consumer/plugin/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
150150
usage_id = request.GET.get('login_hint')
151151
if not usage_id:
152152
log.info('The `login_hint` query param in the request is missing or empty.')
153-
return render(request, 'html/lti_1p3_launch_error.html', status=HTTP_400_BAD_REQUEST)
153+
return render(request, 'html/lti_launch_error.html', status=HTTP_400_BAD_REQUEST)
154154

155155
try:
156156
usage_key = UsageKey.from_string(usage_id)
@@ -161,7 +161,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
161161
exc,
162162
exc_info=True
163163
)
164-
return render(request, 'html/lti_1p3_launch_error.html', status=HTTP_404_NOT_FOUND)
164+
return render(request, 'html/lti_launch_error.html', status=HTTP_404_NOT_FOUND)
165165

166166
try:
167167
lti_config = LtiConfiguration.objects.get(
@@ -173,7 +173,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
173173

174174
if lti_config.version != LtiConfiguration.LTI_1P3:
175175
log.error("The LTI Version of configuration %s is not LTI 1.3", lti_config)
176-
return render(request, 'html/lti_1p3_launch_error.html', status=HTTP_404_NOT_FOUND)
176+
return render(request, 'html/lti_launch_error.html', status=HTTP_404_NOT_FOUND)
177177

178178
context = {}
179179

@@ -269,7 +269,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
269269
exc,
270270
exc_info=True
271271
)
272-
return render(request, 'html/lti_1p3_launch_error.html', context, status=HTTP_400_BAD_REQUEST)
272+
return render(request, 'html/lti_launch_error.html', context, status=HTTP_400_BAD_REQUEST)
273273
except AssertionError as exc:
274274
log.warning(
275275
"Permission on LTI block %r denied for user %r: %s",

lti_consumer/templates/html/lti_1p3_launch_error.html renamed to lti_consumer/templates/html/lti_launch_error.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</head>
88
<body>
99
<p>
10-
<b>{% trans "There was an error while launching the LTI 1.3 tool." %}</b>
10+
<b>{% trans "There was an error while launching the LTI tool." %}</b>
1111
</p>
1212
<p>
1313
{% trans "If you're seeing this on a live course, please contact the course staff." %}

lti_consumer/tests/unit/plugin/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_launch_callback_endpoint_fails(self):
196196
self.assertEqual(response.status_code, 400)
197197

198198
response_body = response.content.decode('utf-8')
199-
self.assertIn("There was an error while launching the LTI 1.3 tool.", response_body)
199+
self.assertIn("There was an error while launching the LTI tool.", response_body)
200200
self.assertNotIn("% trans", response_body)
201201

202202
with patch(

lti_consumer/tests/unit/test_lti_xblock.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,29 +146,40 @@ def test_role(self):
146146
"""
147147
fake_user = Mock()
148148
fake_user.opt_attrs = {
149-
'edx-platform.user_role': 'student'
149+
'edx-platform.user_role': 'student',
150+
'edx-platform.is_authenticated': True,
150151
}
151152
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
152153
self.assertEqual(self.xblock.role, 'Student,Learner')
153154

154155
fake_user.opt_attrs = {
155-
'edx-platform.user_role': 'guest'
156+
'edx-platform.user_role': 'guest',
157+
'edx-platform.is_authenticated': True,
156158
}
157159
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
158160
self.assertEqual(self.xblock.role, 'Student,Learner')
159161

160162
fake_user.opt_attrs = {
161-
'edx-platform.user_role': 'staff'
163+
'edx-platform.user_role': 'staff',
164+
'edx-platform.is_authenticated': True,
162165
}
163166
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
164167
self.assertEqual(self.xblock.role, 'Administrator')
165168

166169
fake_user.opt_attrs = {
167-
'edx-platform.user_role': 'instructor'
170+
'edx-platform.user_role': 'instructor',
171+
'edx-platform.is_authenticated': True,
168172
}
169173
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
170174
self.assertEqual(self.xblock.role, 'Instructor')
171175

176+
fake_user.opt_attrs = {
177+
'edx-platform.user_role': 'student',
178+
'edx-platform.is_authenticated': False,
179+
}
180+
with self.assertRaises(LtiError):
181+
_ = self.xblock.role
182+
172183
def test_course(self):
173184
"""
174185
Test `course` calls modulestore.get_course
@@ -593,7 +604,8 @@ def test_get_real_user_callable(self):
593604
fake_user.emails = [fake_user_email]
594605
fake_username = 'fake'
595606
fake_user.opt_attrs = {
596-
"edx-platform.username": fake_username
607+
"edx-platform.username": fake_username,
608+
"edx-platform.is_authenticated": True,
597609
}
598610

599611
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
@@ -616,14 +628,29 @@ def test_get_real_user_callable_with_language_preference(self):
616628
fake_user.opt_attrs = {
617629
"edx-platform.user_preferences": {
618630
"pref-lang": "en"
619-
}
631+
},
632+
"edx-platform.is_authenticated": True,
620633
}
621634

622635
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
623636

624637
real_user_data = self.xblock.extract_real_user_data()
625638
self.assertEqual(real_user_data['user_language'], pref_language)
626639

640+
def test_unauthenticated_user(self):
641+
"""
642+
Test that an LtiError is raised when the user is unauthenticated.
643+
"""
644+
fake_user = Mock()
645+
fake_user.opt_attrs = {
646+
"edx-platform.is_authenticated": False,
647+
}
648+
649+
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
650+
651+
with self.assertRaises(LtiError):
652+
self.xblock.extract_real_user_data()
653+
627654

628655
class TestStudentView(TestLtiConsumerXBlock):
629656
"""
@@ -734,7 +761,8 @@ def setUp(self):
734761
fake_user.emails = [fake_user_email]
735762
fake_username = 'fake'
736763
fake_user.opt_attrs = {
737-
"edx-platform.username": fake_username
764+
"edx-platform.username": fake_username,
765+
"edx-platform.is_authenticated": True,
738766
}
739767

740768
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
@@ -757,6 +785,36 @@ def test_generate_launch_request_called(self, mock_course):
757785
self.assertEqual(response.status_code, 200)
758786
self.assertEqual(response.content_type, 'text/html')
759787

788+
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.course')
789+
def test_lti_launch_handler_unauthenticated(self, mock_course):
790+
"""
791+
Test that a 400 response an an appropriate template is rendered when a user is unauthenticated
792+
during an LTI launch according to the LMS's user service.
793+
"""
794+
provider = 'lti_provider'
795+
key = 'test'
796+
secret = 'secret'
797+
type(mock_course).lti_passports = PropertyMock(return_value=[f"{provider}:{key}:{secret}"])
798+
799+
fake_user = Mock()
800+
fake_user_email = 'abc@example.com'
801+
fake_user.emails = [fake_user_email]
802+
fake_username = 'fake'
803+
fake_user.opt_attrs = {
804+
"edx-platform.username": fake_username,
805+
"edx-platform.is_authenticated": True,
806+
}
807+
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
808+
809+
request = make_request('', 'GET')
810+
response = self.xblock.lti_launch_handler(request)
811+
812+
self.assertEqual(response.status_code, 400)
813+
self.assertEqual(response.content_type, 'text/html')
814+
815+
response_body = response.body.decode('utf-8')
816+
self.assertIn("There was an error while launching the LTI tool.", response_body)
817+
760818
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.course')
761819
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.user_id', PropertyMock(return_value=FAKE_USER_ID))
762820
def test_publish_tracking_event(self, mock_course):

0 commit comments

Comments
 (0)