Commit 0248f8af by Diana Huang Committed by Adam Palay

Add new clickjacking decorator that whitelists LTI consumers.

parent c76e2492
......@@ -2,6 +2,7 @@
Public views
"""
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.clickjacking import xframe_options_deny
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
......@@ -17,6 +18,7 @@ __all__ = ['signup', 'login_page', 'howitworks']
@ensure_csrf_cookie
@xframe_options_deny
def signup(request):
"""
Display the signup form.
......@@ -34,6 +36,7 @@ def signup(request):
@ssl_login_shortcut
@ensure_csrf_cookie
@xframe_options_deny
def login_page(request):
"""
Display the login form.
......
"""
Decorators that can be used to interact with third_party_auth.
"""
from functools import wraps
from urlparse import urlparse
from django.conf import settings
from django.utils.decorators import available_attrs
from third_party_auth.models import LTIProviderConfig
def xframe_allow_whitelisted(view_func):
"""
Modifies a view function so that its response has the X-Frame-Options HTTP header
set to 'DENY' if the request HTTP referrer is not from a whitelisted hostname.
"""
def wrapped_view(request, *args, **kwargs):
""" Modify the response with the correct X-Frame-Options. """
resp = view_func(request, *args, **kwargs)
x_frame_option = 'DENY'
if settings.FEATURES['ENABLE_THIRD_PARTY_AUTH']:
referer = request.META.get('HTTP_REFERER')
if referer is not None:
parsed_url = urlparse(referer)
hostname = parsed_url.hostname
if LTIProviderConfig.objects.current_set().filter(lti_hostname=hostname, enabled=True).exists():
x_frame_option = 'ALLOW'
resp['X-Frame-Options'] = x_frame_option
return resp
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
......@@ -493,6 +493,15 @@ class LTIProviderConfig(ProviderConfig):
'The name that the LTI Tool Consumer will use to identify itself'
)
)
lti_hostname = models.CharField(
max_length=255,
help_text=(
'The domain that will be acting as the LTI consumer.'
),
db_index=True
)
lti_consumer_secret = models.CharField(
default=long_token,
max_length=255,
......
"""
Tests for third_party_auth decorators.
"""
import ddt
from django.http import HttpResponse
from django.test import RequestFactory
from third_party_auth.decorators import xframe_allow_whitelisted
from third_party_auth.tests.testutil import TestCase
@xframe_allow_whitelisted
def mock_view(_request):
""" A test view for testing purposes. """
return HttpResponse()
@ddt.ddt
class TestXFrameWhitelistDecorator(TestCase):
""" Test the xframe_allow_whitelisted decorator. """
def setUp(self):
super(TestXFrameWhitelistDecorator, self).setUp()
self.configure_lti_provider(name='Test', lti_hostname='localhost', lti_consumer_key='test_key', enabled=True)
self.factory = RequestFactory()
def construct_request(self, referer):
""" Add the given referer to a request and then return it. """
request = self.factory.get('/login')
request.META['HTTP_REFERER'] = referer
return request
@ddt.unpack
@ddt.data(
('http://localhost:8000/login', 'ALLOW'),
('http://not-a-real-domain.com/login', 'DENY'),
(None, 'DENY')
)
def test_x_frame_options(self, url, expected_result):
request = self.construct_request(url)
response = mock_view(request)
self.assertEqual(response['X-Frame-Options'], expected_result)
@ddt.data('http://localhost/login', 'http://not-a-real-domain.com', None)
def test_feature_flag_off(self, url):
with self.settings(FEATURES={'ENABLE_THIRD_PARTY_AUTH': False}):
request = self.construct_request(url)
response = mock_view(request)
self.assertEqual(response['X-Frame-Options'], 'DENY')
......@@ -354,6 +354,24 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
self.assertContains(resp, "Register for Test Microsite")
self.assertContains(resp, "register-form")
def test_login_registration_xframe_protected(self):
resp = self.client.get(
reverse("register_user"),
{},
HTTP_REFERER="http://localhost/iframe"
)
self.assertEqual(resp['X-Frame-Options'], 'DENY')
self.configure_lti_provider(name='Test', lti_hostname='localhost', lti_consumer_key='test_key', enabled=True)
resp = self.client.get(
reverse("register_user"),
HTTP_REFERER="http://localhost/iframe"
)
self.assertEqual(resp['X-Frame-Options'], 'ALLOW')
def _assert_third_party_auth_data(self, response, current_backend, current_provider, providers):
"""Verify that third party auth info is rendered correctly in a DOM data attribute. """
finish_auth_url = None
......
......@@ -34,6 +34,7 @@ from student.views import (
from student.helpers import get_next_url_for_login_page
import third_party_auth
from third_party_auth import pipeline
from third_party_auth.decorators import xframe_allow_whitelisted
from util.bad_request_rate_limiter import BadRequestRateLimiter
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
......@@ -45,6 +46,7 @@ AUDIT_LOG = logging.getLogger("audit")
@require_http_methods(['GET'])
@ensure_csrf_cookie
@xframe_allow_whitelisted
def login_and_registration_form(request, initial_mode="login"):
"""Render the combined login/registration form, defaulting to login
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment