Configure LMS to select oauth2 providing library.
Available backends: * django-oauth-toolkit (DOT) * django-oauth2-provider (DOP) * Use provided client ID to select backend for * AccessToken requests * third party auth-token exchange * Create adapters to isolate library-dependent functionality * Handle django-oauth-toolkit tokens in edX DRF authenticator class MA-1998 MA-2000
This commit is contained in:
111
common/djangoapps/auth_exchange/tests/mixins.py
Normal file
111
common/djangoapps/auth_exchange/tests/mixins.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Mixins to facilitate testing OAuth connections to Django-OAuth-Toolkit or
|
||||
Django-OAuth2-Provider.
|
||||
"""
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
from unittest import skip, expectedFailure
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from lms.djangoapps.oauth_dispatch import adapters
|
||||
from lms.djangoapps.oauth_dispatch.tests.constants import DUMMY_REDIRECT_URL
|
||||
|
||||
from ..views import DOTAccessTokenExchangeView
|
||||
|
||||
|
||||
class DOPAdapterMixin(object):
|
||||
"""
|
||||
Mixin to rewire existing tests to use django-oauth2-provider (DOP) backend
|
||||
|
||||
Overwrites self.client_id, self.access_token, self.oauth2_adapter
|
||||
"""
|
||||
client_id = 'dop_test_client_id'
|
||||
access_token = 'dop_test_access_token'
|
||||
oauth2_adapter = adapters.DOPAdapter()
|
||||
|
||||
def create_public_client(self, user, client_id=None):
|
||||
"""
|
||||
Create an oauth client application that is public.
|
||||
"""
|
||||
return self.oauth2_adapter.create_public_client(
|
||||
name='Test Public Client',
|
||||
user=user,
|
||||
client_id=client_id,
|
||||
redirect_uri=DUMMY_REDIRECT_URL,
|
||||
)
|
||||
|
||||
def create_confidential_client(self, user, client_id=None):
|
||||
"""
|
||||
Create an oauth client application that is confidential.
|
||||
"""
|
||||
return self.oauth2_adapter.create_confidential_client(
|
||||
name='Test Confidential Client',
|
||||
user=user,
|
||||
client_id=client_id,
|
||||
redirect_uri=DUMMY_REDIRECT_URL,
|
||||
)
|
||||
|
||||
def get_token_response_keys(self):
|
||||
"""
|
||||
Return the set of keys provided when requesting an access token
|
||||
"""
|
||||
return {'access_token', 'token_type', 'expires_in', 'scope'}
|
||||
|
||||
|
||||
class DOTAdapterMixin(object):
|
||||
"""
|
||||
Mixin to rewire existing tests to use django-oauth-toolkit (DOT) backend
|
||||
|
||||
Overwrites self.client_id, self.access_token, self.oauth2_adapter
|
||||
"""
|
||||
|
||||
client_id = 'dot_test_client_id'
|
||||
access_token = 'dot_test_access_token'
|
||||
oauth2_adapter = adapters.DOTAdapter()
|
||||
|
||||
def create_public_client(self, user, client_id=None):
|
||||
"""
|
||||
Create an oauth client application that is public.
|
||||
"""
|
||||
return self.oauth2_adapter.create_public_client(
|
||||
name='Test Public Application',
|
||||
user=user,
|
||||
client_id=client_id,
|
||||
redirect_uri=DUMMY_REDIRECT_URL,
|
||||
)
|
||||
|
||||
def create_confidential_client(self, user, client_id=None):
|
||||
"""
|
||||
Create an oauth client application that is confidential.
|
||||
"""
|
||||
return self.oauth2_adapter.create_confidential_client(
|
||||
name='Test Confidential Application',
|
||||
user=user,
|
||||
client_id=client_id,
|
||||
redirect_uri=DUMMY_REDIRECT_URL,
|
||||
)
|
||||
|
||||
def get_token_response_keys(self):
|
||||
"""
|
||||
Return the set of keys provided when requesting an access token
|
||||
"""
|
||||
return {'access_token', 'refresh_token', 'token_type', 'expires_in', 'scope'}
|
||||
|
||||
def test_get_method(self):
|
||||
# Dispatch routes all get methods to DOP, so we test this on the view
|
||||
request_factory = RequestFactory()
|
||||
request = request_factory.get('/oauth2/exchange_access_token/')
|
||||
request.session = {}
|
||||
view = DOTAccessTokenExchangeView.as_view()
|
||||
response = view(request, backend='facebook')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@expectedFailure
|
||||
def test_single_access_token(self):
|
||||
# TODO: Single access tokens not supported yet for DOT (See MA-2122)
|
||||
super(DOTAdapterMixin, self).test_single_access_token()
|
||||
|
||||
@skip("Not supported yet (See MA-2123)")
|
||||
def test_scopes(self):
|
||||
super(DOTAdapterMixin, self).test_scopes()
|
||||
@@ -12,10 +12,12 @@ import httpretty
|
||||
from provider import scope
|
||||
import social.apps.django_app.utils as social_utils
|
||||
|
||||
from auth_exchange.forms import AccessTokenExchangeForm
|
||||
from auth_exchange.tests.utils import AccessTokenExchangeTestMixin
|
||||
from third_party_auth.tests.utils import ThirdPartyOAuthTestMixinFacebook, ThirdPartyOAuthTestMixinGoogle
|
||||
|
||||
from ..forms import AccessTokenExchangeForm
|
||||
from .utils import AccessTokenExchangeTestMixin
|
||||
from .mixins import DOPAdapterMixin, DOTAdapterMixin
|
||||
|
||||
|
||||
class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
|
||||
"""
|
||||
@@ -31,7 +33,7 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
|
||||
self.request.backend = social_utils.load_backend(self.request.social_strategy, self.BACKEND, redirect_uri)
|
||||
|
||||
def _assert_error(self, data, expected_error, expected_error_description):
|
||||
form = AccessTokenExchangeForm(request=self.request, data=data)
|
||||
form = AccessTokenExchangeForm(request=self.request, oauth2_adapter=self.oauth2_adapter, data=data)
|
||||
self.assertEqual(
|
||||
form.errors,
|
||||
{"error": expected_error, "error_description": expected_error_description}
|
||||
@@ -39,7 +41,7 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
|
||||
self.assertNotIn("partial_pipeline", self.request.session)
|
||||
|
||||
def _assert_success(self, data, expected_scopes):
|
||||
form = AccessTokenExchangeForm(request=self.request, data=data)
|
||||
form = AccessTokenExchangeForm(request=self.request, oauth2_adapter=self.oauth2_adapter, data=data)
|
||||
self.assertTrue(form.is_valid())
|
||||
self.assertEqual(form.cleaned_data["user"], self.user)
|
||||
self.assertEqual(form.cleaned_data["client"], self.oauth_client)
|
||||
@@ -49,13 +51,15 @@ class AccessTokenExchangeFormTest(AccessTokenExchangeTestMixin):
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class AccessTokenExchangeFormTestFacebook(
|
||||
class DOPAccessTokenExchangeFormTestFacebook(
|
||||
DOPAdapterMixin,
|
||||
AccessTokenExchangeFormTest,
|
||||
ThirdPartyOAuthTestMixinFacebook,
|
||||
TestCase
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeForm used with Facebook
|
||||
Tests for AccessTokenExchangeForm used with Facebook, tested against
|
||||
django-oauth2-provider (DOP).
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -63,12 +67,46 @@ class AccessTokenExchangeFormTestFacebook(
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class AccessTokenExchangeFormTestGoogle(
|
||||
class DOTAccessTokenExchangeFormTestFacebook(
|
||||
DOTAdapterMixin,
|
||||
AccessTokenExchangeFormTest,
|
||||
ThirdPartyOAuthTestMixinGoogle,
|
||||
TestCase
|
||||
ThirdPartyOAuthTestMixinFacebook,
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeForm used with Google
|
||||
Tests for AccessTokenExchangeForm used with Facebook, tested against
|
||||
django-oauth-toolkit (DOT).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class DOPAccessTokenExchangeFormTestGoogle(
|
||||
DOPAdapterMixin,
|
||||
AccessTokenExchangeFormTest,
|
||||
ThirdPartyOAuthTestMixinGoogle,
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeForm used with Google, tested against
|
||||
django-oauth2-provider (DOP).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class DOTAccessTokenExchangeFormTestGoogle(
|
||||
DOTAdapterMixin,
|
||||
AccessTokenExchangeFormTest,
|
||||
ThirdPartyOAuthTestMixinGoogle,
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeForm used with Google, tested against
|
||||
django-oauth-toolkit (DOT).
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
# pylint: disable=no-member
|
||||
"""
|
||||
Tests for OAuth token exchange views
|
||||
"""
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
import httpretty
|
||||
import provider.constants
|
||||
from provider import scope
|
||||
from provider.oauth2.models import AccessToken, Client
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from auth_exchange.tests.utils import AccessTokenExchangeTestMixin
|
||||
from student.tests.factories import UserFactory
|
||||
from third_party_auth.tests.utils import ThirdPartyOAuthTestMixinFacebook, ThirdPartyOAuthTestMixinGoogle
|
||||
from .mixins import DOPAdapterMixin, DOTAdapterMixin
|
||||
from .utils import AccessTokenExchangeTestMixin
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
|
||||
"""
|
||||
Mixin that defines test cases for AccessTokenExchangeView
|
||||
@@ -27,33 +32,34 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
|
||||
def setUp(self):
|
||||
super(AccessTokenExchangeViewTest, self).setUp()
|
||||
self.url = reverse("exchange_access_token", kwargs={"backend": self.BACKEND})
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
|
||||
def _assert_error(self, data, expected_error, expected_error_description):
|
||||
response = self.client.post(self.url, data)
|
||||
response = self.csrf_client.post(self.url, data)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response["Content-Type"], "application/json")
|
||||
self.assertEqual(
|
||||
json.loads(response.content),
|
||||
{"error": expected_error, "error_description": expected_error_description}
|
||||
{u"error": expected_error, u"error_description": expected_error_description}
|
||||
)
|
||||
self.assertNotIn("partial_pipeline", self.client.session)
|
||||
|
||||
def _assert_success(self, data, expected_scopes):
|
||||
response = self.client.post(self.url, data)
|
||||
response = self.csrf_client.post(self.url, data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "application/json")
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(set(content.keys()), {"access_token", "token_type", "expires_in", "scope"})
|
||||
self.assertEqual(set(content.keys()), self.get_token_response_keys())
|
||||
self.assertEqual(content["token_type"], "Bearer")
|
||||
self.assertLessEqual(
|
||||
timedelta(seconds=int(content["expires_in"])),
|
||||
provider.constants.EXPIRE_DELTA_PUBLIC
|
||||
)
|
||||
self.assertEqual(content["scope"], " ".join(expected_scopes))
|
||||
token = AccessToken.objects.get(token=content["access_token"])
|
||||
self.assertEqual(content["scope"], self.oauth2_adapter.normalize_scopes(expected_scopes))
|
||||
token = self.oauth2_adapter.get_access_token(token_string=content["access_token"])
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.client, self.oauth_client)
|
||||
self.assertEqual(scope.to_names(token.scope), expected_scopes)
|
||||
self.assertEqual(self.oauth2_adapter.get_client_for_token(token), self.oauth_client)
|
||||
self.assertEqual(self.oauth2_adapter.get_token_scope_names(token), expected_scopes)
|
||||
|
||||
def test_single_access_token(self):
|
||||
def extract_token(response):
|
||||
@@ -64,16 +70,15 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
|
||||
|
||||
self._setup_provider_response(success=True)
|
||||
for single_access_token in [True, False]:
|
||||
with mock.patch(
|
||||
"auth_exchange.views.constants.SINGLE_ACCESS_TOKEN",
|
||||
single_access_token
|
||||
):
|
||||
with mock.patch("auth_exchange.views.constants.SINGLE_ACCESS_TOKEN", single_access_token):
|
||||
first_response = self.client.post(self.url, self.data)
|
||||
second_response = self.client.post(self.url, self.data)
|
||||
self.assertEqual(
|
||||
extract_token(first_response) == extract_token(second_response),
|
||||
single_access_token
|
||||
)
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
self.assertEqual(second_response.status_code, 200)
|
||||
self.assertEqual(
|
||||
extract_token(first_response) == extract_token(second_response),
|
||||
single_access_token
|
||||
)
|
||||
|
||||
def test_get_method(self):
|
||||
response = self.client.get(self.url, self.data)
|
||||
@@ -95,10 +100,11 @@ class AccessTokenExchangeViewTest(AccessTokenExchangeTestMixin):
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class AccessTokenExchangeViewTestFacebook(
|
||||
class DOPAccessTokenExchangeViewTestFacebook(
|
||||
DOPAdapterMixin,
|
||||
AccessTokenExchangeViewTest,
|
||||
ThirdPartyOAuthTestMixinFacebook,
|
||||
TestCase
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeView used with Facebook
|
||||
@@ -106,16 +112,48 @@ class AccessTokenExchangeViewTestFacebook(
|
||||
pass
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class DOTAccessTokenExchangeViewTestFacebook(
|
||||
DOTAdapterMixin,
|
||||
AccessTokenExchangeViewTest,
|
||||
ThirdPartyOAuthTestMixinFacebook,
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Rerun AccessTokenExchangeViewTestFacebook tests against DOT backend
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class AccessTokenExchangeViewTestGoogle(
|
||||
class DOPAccessTokenExchangeViewTestGoogle(
|
||||
DOPAdapterMixin,
|
||||
AccessTokenExchangeViewTest,
|
||||
ThirdPartyOAuthTestMixinGoogle,
|
||||
TestCase
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeView used with Google
|
||||
Tests for AccessTokenExchangeView used with Google using
|
||||
django-oauth2-provider backend.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This is necessary because cms does not implement third party auth
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
@httpretty.activate
|
||||
class DOTAccessTokenExchangeViewTestGoogle(
|
||||
DOTAdapterMixin,
|
||||
AccessTokenExchangeViewTest,
|
||||
ThirdPartyOAuthTestMixinGoogle,
|
||||
TestCase,
|
||||
):
|
||||
"""
|
||||
Tests for AccessTokenExchangeView used with Google using
|
||||
django-oauth-toolkit backend.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""
|
||||
Test utilities for OAuth access token exchange
|
||||
"""
|
||||
import provider.constants
|
||||
from social.apps.django_app.default.models import UserSocialAuth
|
||||
|
||||
from social.apps.django_app.default.models import UserSocialAuth
|
||||
from third_party_auth.tests.utils import ThirdPartyOAuthTestMixin
|
||||
|
||||
|
||||
@@ -37,6 +36,12 @@ class AccessTokenExchangeTestMixin(ThirdPartyOAuthTestMixin):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _create_client(self):
|
||||
"""
|
||||
Create an oauth2 client application using class defaults.
|
||||
"""
|
||||
return self.create_public_client(self.user, self.client_id)
|
||||
|
||||
def test_minimal(self):
|
||||
self._setup_provider_response(success=True)
|
||||
self._assert_success(self.data, expected_scopes=[])
|
||||
@@ -61,12 +66,12 @@ class AccessTokenExchangeTestMixin(ThirdPartyOAuthTestMixin):
|
||||
)
|
||||
|
||||
def test_confidential_client(self):
|
||||
self.oauth_client.client_type = provider.constants.CONFIDENTIAL
|
||||
self.oauth_client.save()
|
||||
self.data['client_id'] += '_confidential'
|
||||
self.oauth_client = self.create_confidential_client(self.user, self.data['client_id'])
|
||||
self._assert_error(
|
||||
self.data,
|
||||
"invalid_client",
|
||||
"test_client_id is not a public client"
|
||||
"{}_confidential is not a public client".format(self.client_id),
|
||||
)
|
||||
|
||||
def test_inactive_user(self):
|
||||
|
||||
Reference in New Issue
Block a user