diff --git a/cms/envs/common.py b/cms/envs/common.py index df800bea0001..d933a8161037 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -438,6 +438,7 @@ 'style-vendor': { 'source_filenames': [ 'css/vendor/normalize.css', + 'css/vendor/office365.css', 'css/vendor/font-awesome.css', 'css/vendor/html5-input-polyfills/number-polyfill.css', 'js/vendor/CodeMirror/codemirror.css', diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index 2ff7ea67b05d..ab99b588f317 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -56,7 +56,7 @@ def B(*args, **kwargs): See http://psa.matiasaguirre.net/docs/pipeline.html for more docs. """ - +import pkg_resources import random import string # pylint: disable-msg=deprecated-module from collections import OrderedDict @@ -556,7 +556,21 @@ def set_logged_in_cookie(backend=None, user=None, request=None, auth_entry=None, # Check that the cookie isn't already set. # This ensures that we allow the user to continue to the next # pipeline step once he/she has the cookie set by this step. + + try: + if not hasattr(request, 'COOKIES'): + + version = pkg_resources.get_distribution("python-social-auth").version + version_parts = tuple([int(x) for x in version.split('.') if x.isdigit()]) + + if float(str(version_parts[0]) + "." + str(version_parts[1])) >= 0.2: + return; + + except Exception: + pass + has_cookie = student.helpers.is_logged_in_cookie_set(request) + if not has_cookie: try: redirect_url = get_complete_url(backend.name) diff --git a/common/djangoapps/third_party_auth/provider.py b/common/djangoapps/third_party_auth/provider.py index 4fc1de6a8d23..d76ee9bbb6f1 100644 --- a/common/djangoapps/third_party_auth/provider.py +++ b/common/djangoapps/third_party_auth/provider.py @@ -4,7 +4,7 @@ invoke the Django armature. """ -from social.backends import google, linkedin, facebook +from social.backends import google, linkedin, facebook, azuread _DEFAULT_ICON_CLASS = 'fa-signin' @@ -21,9 +21,11 @@ class BaseProvider(object): # String. Name of the FontAwesome glyph to use for sign in buttons (or the # name of a user-supplied custom glyph that is present at runtime). ICON_CLASS = _DEFAULT_ICON_CLASS - # String. User-facing name of the provider. Must be unique across all - # enabled providers. Will be presented in the UI. + # String. Name of the provider. Must be unique across all enabled providers. NAME = None + # String. User-facing name of the provider that shows up in the login button text. + # Can contain blanks and will be presented in the UI. + UINAME = None # Dict of string -> object. Settings that will be merged into Django's # settings instance. In most cases the value will be None, since real # values are merged from .json files (foo.auth.json; foo.env.json) onto the @@ -115,7 +117,7 @@ class GoogleOauth2(BaseProvider): BACKEND_CLASS = google.GoogleOAuth2 ICON_CLASS = 'fa-google-plus' - NAME = 'Google' + NAME = UINAME = 'Google' SETTINGS = { 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': None, 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': None, @@ -135,7 +137,7 @@ class LinkedInOauth2(BaseProvider): BACKEND_CLASS = linkedin.LinkedinOAuth2 ICON_CLASS = 'fa-linkedin' - NAME = 'LinkedIn' + NAME = UINAME = 'LinkedIn' SETTINGS = { 'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': None, 'SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET': None, @@ -155,7 +157,7 @@ class FacebookOauth2(BaseProvider): BACKEND_CLASS = facebook.FacebookOAuth2 ICON_CLASS = 'fa-facebook' - NAME = 'Facebook' + NAME = UINAME = 'Facebook' SETTINGS = { 'SOCIAL_AUTH_FACEBOOK_KEY': None, 'SOCIAL_AUTH_FACEBOOK_SECRET': None, @@ -170,6 +172,28 @@ def get_name(cls, provider_details): return provider_details.get('fullname') +class AzureADOauth2(BaseProvider): + """Provider for Microsoft Azure Active Directory's Oauth2 auth system.""" + + BACKEND_CLASS = azuread.AzureADOAuth2 + ICON_CLASS = 'img-office365' + NAME = 'AzureAD' + UINAME = 'Office 365' + SETTINGS = { + 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY': None, + 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET': None, + 'SOCIAL_AUTH_AZUREAD_OAUTH2_RESOURCE': None + } + + @classmethod + def get_email(cls, provider_details): + return provider_details.get('email') + + @classmethod + def get_name(cls, provider_details): + return provider_details.get('fullname') + + class Registry(object): """Singleton registry of third-party auth providers. diff --git a/common/djangoapps/third_party_auth/tests/specs/test_azuread.py b/common/djangoapps/third_party_auth/tests/specs/test_azuread.py new file mode 100644 index 000000000000..849c24f05359 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/specs/test_azuread.py @@ -0,0 +1,53 @@ +"""Integration tests for AzureAd providers.""" + +from third_party_auth import provider +from third_party_auth.tests.specs import base + + +class AzureADOAuth2IntegrationTest(base.Oauth2IntegrationTest): + """Integration tests for provider.AzureADOAuth2.""" + + PROVIDER_CLASS = provider.AzureADOauth2 + PROVIDER_SETTINGS = { + 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY': 'azure_oauth2_key', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET': 'azure_oauth2_secret', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_RESOURCE': 'https://mysite-my.sharepoint.com' + } + TOKEN_RESPONSE_DATA = { + 'access_token': 'foobar', + 'token_type': 'bearer', + 'id_token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83Mjc0MDZhYy03MDY4' + 'LTQ4ZmEtOTJiOS1jMmQ2NzIxMWJjNTAvIiwiaWF0IjpudWxsLCJleHAiOm51bGwsImF1ZCI6IjAyOWNjMDEwLWJiNzQtNGQyY' + 'i1hMDQwLWY5Y2VkM2ZkMmM3NiIsInN1YiI6InFVOHhrczltSHFuVjZRMzR6aDdTQVpvY2loOUV6cnJJOW1wVlhPSWJWQTgiLC' + 'J2ZXIiOiIxLjAiLCJ0aWQiOiI3Mjc0MDZhYy03MDY4LTQ4ZmEtOTJiOS1jMmQ2NzIxMWJjNTAiLCJvaWQiOiI3ZjhlMTk2OS0' + '4YjgxLTQzOGMtOGQ0ZS1hZDZmNTYyYjI4YmIiLCJ1cG4iOiJmb29iYXJAdGVzdC5vbm1pY3Jvc29mdC5jb20iLCJnaXZlbl9u' + 'YW1lIjoiZm9vIiwiZmFtaWx5X25hbWUiOiJiYXIiLCJuYW1lIjoiZm9vIGJhciIsInVuaXF1ZV9uYW1lIjoiZm9vYmFyQHRlc' + '3Qub25taWNyb3NvZnQuY29tIiwicHdkX2V4cCI6IjQ3MzMwOTY4IiwicHdkX3VybCI6Imh0dHBzOi8vcG9ydGFsLm1pY3Jvc2' + '9mdG9ubGluZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn2g7BjX5JxNe8skYlK4PdhiLz4', + 'expires_in': 3600, + 'expires_on': 1423650396, + 'not_before': 1423646496 + } + + USER_RESPONSE_DATA = { + "iss": "https://sts.windows.net/727406ac-7068-48fa-92b9-c2d67211bc50/", + "iat": 'null', + "exp": 'null', + "aud": "029cc010-bb74-4d2b-a040-f9ced3fd2c76", + "sub": "qU8xks9mHqnV6Q34zh7SAZocih9EzrrI9mpVXOIbVA8", + "ver": "1.0", + "tid": "727406ac-7068-48fa-92b9-c2d67211bc50", + "oid": "7f8e1969-8b81-438c-8d4e-ad6f562b28bb", + "upn": "foobar@test.onmicrosoft.com", + "given_name": "foo", + "family_name": "bar", + "name": "foo bar", + "unique_name": "foobar@test.onmicrosoft.com", + "pwd_exp": "47330968", + "pwd_url": "https://portal.microsoftonline.com/ChangePassword.aspx" + } + + def get_username(self): + response_data = self.get_response_data() + return response_data.get('upn') + diff --git a/common/static/css/vendor/office365.css b/common/static/css/vendor/office365.css new file mode 100644 index 000000000000..07c07580cc02 --- /dev/null +++ b/common/static/css/vendor/office365.css @@ -0,0 +1,7 @@ +.img-office365{ + background-image: url('../../images/office365.png'); + vertical-align: sub; + display: inline-block; + height: 21px; + width: 22px; +} \ No newline at end of file diff --git a/common/static/images/office365.png b/common/static/images/office365.png new file mode 100644 index 000000000000..77be99a62d1a Binary files /dev/null and b/common/static/images/office365.png differ diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index faf1c0a84ba5..28599b878ada 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -374,6 +374,7 @@ def account_settings_context(request): context['auth']['providers'] = [{ 'name': state.provider.NAME, # The name of the provider e.g. Facebook + 'uiname': state.provider.UINAME, # The ui name of the provider e.g. Office 365 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. diff --git a/lms/envs/common.py b/lms/envs/common.py index c00c94fdf60a..f15bb6b3a681 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1254,6 +1254,7 @@ 'style-vendor': { 'source_filenames': [ 'css/vendor/font-awesome.css', + 'css/vendor/office365.css', 'css/vendor/jquery.qtip.min.css', 'css/vendor/responsive-carousel/responsive-carousel.css', 'css/vendor/responsive-carousel/responsive-carousel.slide.css', diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 07daa251748f..76fd02923dc6 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -133,9 +133,9 @@ fields: _.map(authData.providers, function(provider) { return { 'view': new AccountSettingsFieldViews.AuthFieldView({ - title: provider.name, + title: provider.uiname, screenReaderTitle: interpolate_text( - gettext("Connect your {accountName} account"), {accountName: provider['name']} + gettext("Connect your {accountName} account"), {accountName: provider['uiname']} ), valueAttribute: 'auth-' + provider.name.toLowerCase(), helpMessage: '', diff --git a/lms/templates/dashboard/_dashboard_third_party_error.html b/lms/templates/dashboard/_dashboard_third_party_error.html index 99ba0ae4fbb7..90dc9023e466 100644 --- a/lms/templates/dashboard/_dashboard_third_party_error.html +++ b/lms/templates/dashboard/_dashboard_third_party_error.html @@ -5,7 +5,7 @@
${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name='{duplicate_provider}'.format(duplicate_provider=duplicate_provider.NAME), platform_name=platform_name)}
+${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name='{duplicate_provider}'.format(duplicate_provider=duplicate_provider.UINAME), platform_name=platform_name)}