Specially handle login redirect for mobile apps
This commit is contained in:
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
60
openedx/core/lib/mobile_utils.py
Normal file
60
openedx/core/lib/mobile_utils.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user