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:
J. Cliff Dyer
2016-02-01 20:36:35 +00:00
parent 88fef8b2a4
commit 1df040228a
29 changed files with 1114 additions and 92 deletions

View File

@@ -12,8 +12,8 @@ from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.test import override_settings, TestCase
from edx_rest_api_client.client import EdxRestApiClient
from edx_oauth2_provider.tests.factories import ClientFactory
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.programs.tasks.v1 import tasks

View File

@@ -2,10 +2,13 @@
import logging
import django.utils.timezone
from rest_framework.authentication import SessionAuthentication
from rest_framework import exceptions as drf_exceptions
from rest_framework_oauth.authentication import OAuth2Authentication
from rest_framework_oauth.compat import oauth2_provider, provider_now
from provider.oauth2 import models as dop_models
from oauth2_provider import models as dot_models
from openedx.core.lib.api.exceptions import AuthenticationFailed
@@ -114,21 +117,44 @@ class OAuth2AuthenticationAllowInactiveUser(OAuth2Authentication):
def authenticate_credentials(self, request, access_token):
"""
Authenticate the request, given the access token.
Overrides base class implementation to discard failure if user is inactive.
Overrides base class implementation to discard failure if user is
inactive.
"""
token_query = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user')
token = token_query.filter(token=access_token).first()
token = self.get_access_token(access_token)
if not token:
raise AuthenticationFailed({
u'error_code': OAUTH2_TOKEN_ERROR_NONEXISTENT,
u'developer_message': u'The provided access token does not match any valid tokens.'
})
# provider_now switches to timezone aware datetime when
# the oauth2_provider version supports it.
elif token.expires < provider_now():
elif token.expires < django.utils.timezone.now():
raise AuthenticationFailed({
u'error_code': OAUTH2_TOKEN_ERROR_EXPIRED,
u'developer_message': u'The provided access token has expired and is no longer valid.',
})
else:
return token.user, token
def get_access_token(self, access_token):
"""
Return a valid access token that exists in one of our OAuth2 libraries,
or None if no matching token is found.
"""
return self._get_dot_token(access_token) or self._get_dop_token(access_token)
def _get_dop_token(self, access_token):
"""
Return a valid access token stored by django-oauth2-provider (DOP), or
None if no matching token is found.
"""
token_query = dop_models.AccessToken.objects.select_related('user')
return token_query.filter(token=access_token).first()
def _get_dot_token(self, access_token):
"""
Return a valid access token stored by django-oauth-toolkit (DOT), or
None if no matching token is found.
"""
token_query = dot_models.AccessToken.objects.select_related('user')
return token_query.filter(token=access_token).first()

View File

@@ -19,6 +19,7 @@ from django.utils import unittest
from django.utils.http import urlencode
from mock import patch
from nose.plugins.attrib import attr
from oauth2_provider import models as dot_models
from rest_framework import exceptions
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
@@ -28,6 +29,7 @@ from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
from rest_framework_jwt.settings import api_settings
from lms.djangoapps.oauth_dispatch import adapters
from openedx.core.lib.api import authentication
from openedx.core.lib.api.tests.mixins import JwtMixin
from provider import constants, scope
@@ -84,6 +86,8 @@ class OAuth2Tests(TestCase):
def setUp(self):
super(OAuth2Tests, self).setUp()
self.dop_adapter = adapters.DOPAdapter()
self.dot_adapter = adapters.DOTAdapter()
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
@@ -95,24 +99,35 @@ class OAuth2Tests(TestCase):
self.ACCESS_TOKEN = 'access_token' # pylint: disable=invalid-name
self.REFRESH_TOKEN = 'refresh_token' # pylint: disable=invalid-name
self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create(
client_id=self.CLIENT_ID,
client_secret=self.CLIENT_SECRET,
redirect_uri='',
client_type=0,
self.dop_oauth2_client = self.dop_adapter.create_public_client(
name='example',
user=None,
user=self.user,
client_id=self.CLIENT_ID,
redirect_uri='https://example.edx/redirect',
)
self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create(
token=self.ACCESS_TOKEN,
client=self.oauth2_client,
client=self.dop_oauth2_client,
user=self.user,
)
self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create(
user=self.user,
access_token=self.access_token,
client=self.oauth2_client
client=self.dop_oauth2_client,
)
self.dot_oauth2_client = self.dot_adapter.create_public_client(
name='example',
user=self.user,
client_id='dot-client-id',
redirect_uri='https://example.edx/redirect',
)
self.dot_access_token = dot_models.AccessToken.objects.create(
user=self.user,
token='dot-access-token',
application=self.dot_oauth2_client,
expires=datetime.now() + timedelta(days=30),
)
# This is the a change we've made from the django-rest-framework-oauth version
@@ -182,6 +197,10 @@ class OAuth2Tests(TestCase):
response = self.get_with_bearer_token('/oauth2-test/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get_form_passing_auth_with_dot(self):
response = self.get_with_bearer_token('/oauth2-test/', token=self.dot_access_token.token)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
def test_post_form_passing_auth_url_transport(self):
"""Ensure GETing form over OAuth with correct client credentials in form data succeed"""