Add IdentityServer3 Backend (#20275)
* Add IdentityServer3 Backend This adds a backend for users who want to use IdentityServer3 as their SSO provider. It can be used with the OAuth2ProviderConfig in django admin to point to an external provider.
This commit is contained in:
69
common/djangoapps/third_party_auth/identityserver3.py
Normal file
69
common/djangoapps/third_party_auth/identityserver3.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#pylint: disable=W0223
|
||||
"""
|
||||
python-social-auth backend for use with IdentityServer3
|
||||
docs: https://identityserver.github.io/Documentation/docsv2/endpoints/authorization.html
|
||||
docs for adding a new backend to python-social-auth:
|
||||
https://python-social-auth.readthedocs.io/en/latest/backends/implementation.html#oauth
|
||||
"""
|
||||
from social_core.backends.oauth import BaseOAuth2
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class IdentityServer3(BaseOAuth2):
|
||||
"""
|
||||
An extension of the BaseOAuth2 for use with an IdentityServer3 service.
|
||||
"""
|
||||
|
||||
name = "identityServer3"
|
||||
REDIRECT_STATE = False
|
||||
ACCESS_TOKEN_METHOD = 'POST'
|
||||
DEFAULT_SCOPE = ['openid']
|
||||
ID_KEY = "sub"
|
||||
|
||||
def authorization_url(self):
|
||||
return self.get_config().get_setting('auth_url')
|
||||
|
||||
def access_token_url(self):
|
||||
return self.get_config().get_setting('token_url')
|
||||
|
||||
def get_redirect_uri(self, state=None):
|
||||
return self.get_config().get_setting('redirect_url')
|
||||
|
||||
def user_data(self, access_token, *args, **kwargs):
|
||||
"""
|
||||
consumes the access_token to get data about the user logged
|
||||
into the service.
|
||||
"""
|
||||
url = self.get_config().get_setting('user_info_url')
|
||||
# The access token returned from the service's token route.
|
||||
header = {"Authorization": u"Bearer %s" % access_token}
|
||||
return self.get_json(url, headers=header)
|
||||
|
||||
def get_user_details(self, response):
|
||||
"""
|
||||
Return details about the user account from the service
|
||||
"""
|
||||
details = {"fullname": response["name"], "email": response["email"]}
|
||||
return details
|
||||
|
||||
def get_user_id(self, details, response):
|
||||
"""
|
||||
Gets the unique identifier from the user. this is
|
||||
how edx knows who's logging in, and if they have an account
|
||||
already through edx. IdentityServer emits standard claim type of sub
|
||||
to identify the user according to these docs:
|
||||
https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
"""
|
||||
try:
|
||||
user_id = response.get(self.ID_KEY)
|
||||
except KeyError:
|
||||
user_id = None
|
||||
return user_id
|
||||
|
||||
def get_config(self):
|
||||
return self._id3_config
|
||||
|
||||
@cached_property
|
||||
def _id3_config(self):
|
||||
from .models import OAuth2ProviderConfig
|
||||
return OAuth2ProviderConfig.current("identityServer3")
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Unit tests for the IdentityServer3 OAuth2 Backend
|
||||
"""
|
||||
import unittest
|
||||
from third_party_auth.identityserver3 import IdentityServer3
|
||||
from third_party_auth.tests import testutil
|
||||
|
||||
|
||||
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
|
||||
class IdentityServer3Test(testutil.TestCase):
|
||||
"""
|
||||
Unit tests for the IdentityServer3 OAuth2 Backend
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(IdentityServer3Test, self).setUp()
|
||||
self.id3_instance = IdentityServer3()
|
||||
|
||||
def test_proper_get_of_user_id(self):
|
||||
"""
|
||||
make sure the "sub" claim works properly to grab user Id
|
||||
"""
|
||||
response = {"sub": 1, "email": "example@example.com"}
|
||||
self.assertEqual(self.id3_instance.get_user_id({}, response), 1)
|
||||
|
||||
def test_key_error_thrown_with_no_sub(self):
|
||||
"""
|
||||
test that a KeyError is thrown if the "sub" claim does not exist
|
||||
"""
|
||||
response = {"id": 1}
|
||||
self.assertRaises(KeyError, self.id3_instance.get_user_id({}, response))
|
||||
|
||||
def test_proper_config_access(self):
|
||||
"""
|
||||
test that the IdentityServer3 model properly grabs OAuth2Configs
|
||||
"""
|
||||
provider_config = self.configure_identityServer3_provider(backend_name="identityServer3")
|
||||
self.assertEqual(self.id3_instance.get_config(), provider_config)
|
||||
|
||||
def test_config_after_updating(self):
|
||||
"""
|
||||
Make sure when the OAuth2Config for this backend is updated, the new config is properly grabbed
|
||||
"""
|
||||
original_provider_config = self.configure_identityServer3_provider(enabled=True, slug="original")
|
||||
updated_provider_config = self.configure_identityServer3_provider(
|
||||
slug="updated",
|
||||
backend_name="identityServer3"
|
||||
)
|
||||
self.assertEqual(self.id3_instance.get_config(), updated_provider_config)
|
||||
self.assertNotEqual(self.id3_instance.get_config(), original_provider_config)
|
||||
@@ -156,6 +156,12 @@ class ThirdPartyAuthTestMixin(object):
|
||||
kwargs.setdefault("backend_name", "dummy")
|
||||
return cls.configure_oauth_provider(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def configure_identityServer3_provider(cls, **kwargs):
|
||||
kwargs.setdefault("name", "identityServer3TestConfig")
|
||||
kwargs.setdefault("backend_name", "identityServer3")
|
||||
return cls.configure_oauth_provider(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def verify_user_email(cls, email):
|
||||
""" Mark the user with the given email as verified """
|
||||
|
||||
@@ -678,6 +678,7 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
|
||||
'social_core.backends.linkedin.LinkedinOAuth2',
|
||||
'social_core.backends.facebook.FacebookOAuth2',
|
||||
'social_core.backends.azuread.AzureADOAuth2',
|
||||
'third_party_auth.identityserver3.IdentityServer3',
|
||||
'third_party_auth.saml.SAMLAuthBackend',
|
||||
'third_party_auth.lti.LTIAuthBackend',
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user