From e76e05fa77e5b0132f7a6b567e6015e770668250 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Wed, 9 Dec 2015 22:10:39 -0500 Subject: [PATCH] Specially handle login redirect for mobile apps --- lms/envs/common.py | 5 ++ .../djangoapps/safe_sessions/middleware.py | 8 +++ .../safe_sessions/tests/test_middleware.py | 61 +++++++++++++++---- openedx/core/lib/mobile_utils.py | 60 ++++++++++++++++++ 4 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 openedx/core/lib/mobile_utils.py diff --git a/lms/envs/common.py b/lms/envs/common.py index d60dbba7a4..792213fcf3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2706,3 +2706,8 @@ MAX_BOOKMARKS_PER_COURSE = 100 # lms.env.json file. REGISTRATION_EXTENSION_FORM = None + +# Identifier included in the User Agent from open edX mobile apps. +MOBILE_APP_USER_AGENT_REGEXES = [ + r'edX/org.edx.mobile', +] diff --git a/openedx/core/djangoapps/safe_sessions/middleware.py b/openedx/core/djangoapps/safe_sessions/middleware.py index 0f2e7f4503..cb4714a6ab 100644 --- a/openedx/core/djangoapps/safe_sessions/middleware.py +++ b/openedx/core/djangoapps/safe_sessions/middleware.py @@ -61,10 +61,12 @@ from django.contrib.auth import SESSION_KEY from django.contrib.auth.views import redirect_to_login from django.contrib.sessions.middleware import SessionMiddleware from django.core import signing +from django.http import HttpResponse from django.utils.crypto import get_random_string from hashlib import sha256 from logging import getLogger +from openedx.core.lib.mobile_utils import is_request_from_mobile_app log = getLogger(__name__) @@ -339,6 +341,12 @@ class SafeSessionMiddleware(SessionMiddleware): cookie and redirects the user to the login page. """ _mark_cookie_for_deletion(request) + + # Mobile apps have custom handling of authentication failures. They + # should *not* be redirected to the website's login page. + if is_request_from_mobile_app(request): + return HttpResponse(status=401) + return redirect_to_login(request.path) @staticmethod diff --git a/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py b/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py index 4a525866b6..e23eb4790a 100644 --- a/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py +++ b/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py @@ -10,6 +10,7 @@ from django.contrib.auth.models import AnonymousUser from django.http import HttpResponse, HttpResponseRedirect, SimpleCookie from django.test import TestCase from django.test.client import RequestFactory +from django.test.utils import override_settings from mock import patch from student.tests.factories import UserFactory @@ -18,6 +19,17 @@ from ..middleware import SafeSessionMiddleware, SafeCookieData from .test_utils import TestSafeSessionsLogMixin +def create_mock_request(): + """ + Creates and returns a mock request object for testing. + """ + request = RequestFactory() + request.COOKIES = {} + request.META = {} + request.path = '/' + return request + + class TestSafeSessionProcessRequest(TestSafeSessionsLogMixin, TestCase): """ Test class for SafeSessionMiddleware.process_request @@ -25,9 +37,7 @@ class TestSafeSessionProcessRequest(TestSafeSessionsLogMixin, TestCase): def setUp(self): super(TestSafeSessionProcessRequest, self).setUp() self.user = UserFactory.create() - self.request = RequestFactory() - self.request.COOKIES = {} - self.request.path = '/' + self.request = create_mock_request() def assert_response(self, safe_cookie_data=None, success=True): """ @@ -128,9 +138,7 @@ class TestSafeSessionProcessResponse(TestSafeSessionsLogMixin, TestCase): def setUp(self): super(TestSafeSessionProcessResponse, self).setUp() self.user = UserFactory.create() - self.request = RequestFactory() - self.request.COOKIES = {} - self.request.path = '/' + self.request = create_mock_request() self.request.session = {} self.client.response = HttpResponse() self.client.response.cookies = SimpleCookie() @@ -230,9 +238,7 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase): def setUp(self): super(TestSafeSessionMiddleware, self).setUp() self.user = UserFactory.create() - self.request = RequestFactory() - self.request.COOKIES = {} - self.request.path = '/' + self.request = create_mock_request() self.client.response = HttpResponse() self.client.response.cookies = SimpleCookie() @@ -246,7 +252,10 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase): settings.SESSION_COOKIE_NAME ] - def test_success(self): + def verify_success(self): + """ + Verifies success path. + """ self.client.login(username=self.user.username, password='test') self.request.user = self.user @@ -265,11 +274,27 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase): response = SafeSessionMiddleware().process_response(self.request, self.client.response) self.assertEquals(response.status_code, 200) - def test_error(self): + def test_success(self): + self.verify_success() + + def test_success_from_mobile_web_view(self): + self.request.path = '/xblock/block-v1:org+course+run+type@html+block@block_id' + self.verify_success() + + @override_settings(MOBILE_APP_USER_AGENT_REGEXES=[r'open edX Mobile App']) + def test_success_from_mobile_app(self): + self.request.META = {'HTTP_USER_AGENT': 'open edX Mobile App Version 2.1'} + self.verify_success() + + def verify_error(self, expected_response_status): + """ + Verifies error path. + """ self.request.COOKIES[settings.SESSION_COOKIE_NAME] = 'not-a-safe-cookie' with self.assert_parse_error(): - SafeSessionMiddleware().process_request(self.request) + request_response = SafeSessionMiddleware().process_request(self.request) + self.assertEquals(request_response.status_code, expected_response_status) self.assertTrue(self.request.need_to_delete_cookie) self.cookies_from_request_to_response() @@ -277,3 +302,15 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase): with patch('django.http.HttpResponse.set_cookie') as mock_delete_cookie: SafeSessionMiddleware().process_response(self.request, self.client.response) self.assertTrue(mock_delete_cookie.called) + + def test_error(self): + self.verify_error(302) + + def test_error_from_mobile_web_view(self): + self.request.path = '/xblock/block-v1:org+course+run+type@html+block@block_id' + self.verify_error(401) + + @override_settings(MOBILE_APP_USER_AGENT_REGEXES=[r'open edX Mobile App']) + def test_error_from_mobile_app(self): + self.request.META = {'HTTP_USER_AGENT': 'open edX Mobile App Version 2.1'} + self.verify_error(401) diff --git a/openedx/core/lib/mobile_utils.py b/openedx/core/lib/mobile_utils.py new file mode 100644 index 0000000000..0db979c023 --- /dev/null +++ b/openedx/core/lib/mobile_utils.py @@ -0,0 +1,60 @@ +""" +Common utilities related to the mobile apps. +""" + +import re +from django.conf import settings + + +def is_request_from_mobile_app(request): + """ + Returns whether the given request was made by an open edX mobile app, + either natively or through the mobile web view. + + Note: The check for the user agent works only for mobile apps version 2.1 + and higher. Previous apps did not update their user agents to include the + distinguishing string. + + The check for the web view is a temporary check that works for mobile apps + version 2.0 and higher. See is_request_from_mobile_web_view for more + information. + + Args: + request (HttpRequest) + """ + if is_request_from_mobile_web_view(request): + return True + + if getattr(settings, 'MOBILE_APP_USER_AGENT_REGEXES', None): + user_agent = request.META.get('HTTP_USER_AGENT') + if user_agent: + for user_agent_regex in settings.MOBILE_APP_USER_AGENT_REGEXES: + if re.match(user_agent_regex, user_agent): + return True + + return False + + +PATHS_ACCESSED_BY_MOBILE_WITH_SESSION_COOKIES = [ + r'^/xblock/{usage_key_string}$'.format(usage_key_string=settings.USAGE_KEY_PATTERN), +] + + +def is_request_from_mobile_web_view(request): + """ + Returns whether the given request was made by an open edX mobile web + view using a session cookie. + + Args: + request (HttpRequest) + """ + + # TODO (MA-1825): This is a TEMPORARY HACK until all of the version 2.0 + # iOS mobile apps have updated. The earlier versions didn't update their + # user agents so we are checking for the specific URLs that are + # accessed through the mobile web view. + for mobile_path in PATHS_ACCESSED_BY_MOBILE_WITH_SESSION_COOKIES: + if re.match(mobile_path, request.path): + return True + + return False