From 3dcd3e85789fe5f5a7fe4dc5cc9544afc985524b Mon Sep 17 00:00:00 2001 From: vinhub Date: Thu, 28 May 2015 12:09:32 -0700 Subject: [PATCH] WIP: Adding Office 365 login support using Azure Active Directory OAuth2 provider and related changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code is working, but it is WIP because we have a couple of issues that we would like to get reviewed: 1. Python social auth currently does not have support for Azure AD provider. We have sent a PR to them for that in parallel and it is currently being reviewed. But this means that in order for edX to use Azure AD provider, you have to upgrade to the new version of python social auth. In this PR, we are pointing to a version of python social auth with our changes since our PR has still not been merged in. Note that python social auth 0.2.x may have some backward compatibility issues with 0.1.x version and we noticed one and have added a patch to edX code for that. (We noticed these issues with Google’s provider also.) We will update the PR after python social auth people have merged our changes and also you have reviewed our patch to edX to make them work. 2. We need to show an “Office 365” icon in the login button but there is no FontAwesome icon glyph for Office 365. So we have added support for png icons. Also, since in our case the provider name is AzureAD and login name is Office 365 (with a space in it), we have added a UINAME property in addition to NAME property in the provider and adjusted various pieces of UI that use them. We have made sure the other providers NAME and UINAME are the same, so they should be unaffected. --- cms/envs/common.py | 1 + .../djangoapps/third_party_auth/pipeline.py | 16 +++++- .../djangoapps/third_party_auth/provider.py | 36 ++++++++++-- .../tests/specs/test_azuread.py | 53 ++++++++++++++++++ common/static/css/vendor/office365.css | 7 +++ common/static/images/office365.png | Bin 0 -> 439 bytes lms/djangoapps/student_account/views.py | 1 + lms/envs/common.py | 1 + .../views/account_settings_factory.js | 4 +- .../_dashboard_third_party_error.html | 2 +- lms/templates/login.html | 2 +- lms/templates/register.html | 2 +- .../student_account/login.underscore | 2 +- .../student_account/register.underscore | 2 +- .../student_profile/third_party_auth.html | 2 +- requirements/edx/base.txt | 2 +- 16 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 common/djangoapps/third_party_auth/tests/specs/test_azuread.py create mode 100644 common/static/css/vendor/office365.css create mode 100644 common/static/images/office365.png 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 0000000000000000000000000000000000000000..77be99a62d1a7159a9f50d68deb583cd5e00e64b GIT binary patch literal 439 zcmV;o0Z9IdP)^6vuz(K2zjnAqxvdnjvLjVp?aVXrlYmu<>P6a;4}7-O{r|Bo`oOUmFfxC{nBl_y|Pq2sUM)RQ(W zI7(bCH&yT4{2Ms5&&uflHI>5XYXwV}F6{`{V(~C|qIf=d2p(D-{NqROxiqOT2}FRl zj7!BwaB@saA{3>rAUItYF5LnVgmL&XrokQ%wox{m09!4)#7hQ$HyEVCd11)${_("Could Not Link Accounts")}
## Translators: this message is displayed when a user tries to link their account with a third-party authentication provider (for example, Google or LinkedIn) with a given edX account, but their third-party account is already associated with another edX account. provider_name is the name of the third-party authentication provider, and platform_name is the name of the edX deployment. -

${_("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)}

diff --git a/lms/templates/login.html b/lms/templates/login.html index 1b80af903d8e..f8e45db9909e 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -220,7 +220,7 @@

${_('Account Preferences')}

% for enabled in provider.Registry.enabled(): ## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn). - + % endfor diff --git a/lms/templates/register.html b/lms/templates/register.html index d9d0d7e91d81..92df7c7ac3ff 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -126,7 +126,7 @@

${_("The following errors occurred while processing yo % for enabled in provider.Registry.enabled(): ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). - + % endfor diff --git a/lms/templates/student_account/login.underscore b/lms/templates/student_account/login.underscore index 2a7294adfa68..1110e166a24a 100644 --- a/lms/templates/student_account/login.underscore +++ b/lms/templates/student_account/login.underscore @@ -44,7 +44,7 @@ if ( provider.loginUrl ) { %> <% } }); %> diff --git a/lms/templates/student_account/register.underscore b/lms/templates/student_account/register.underscore index a5de5b5f294b..6929663d41ef 100644 --- a/lms/templates/student_account/register.underscore +++ b/lms/templates/student_account/register.underscore @@ -23,7 +23,7 @@ if ( provider.registerUrl ) { %> <% } }); %> diff --git a/lms/templates/student_profile/third_party_auth.html b/lms/templates/student_profile/third_party_auth.html index 33c948d3fdac..dd4b77f4c6a1 100644 --- a/lms/templates/student_profile/third_party_auth.html +++ b/lms/templates/student_profile/third_party_auth.html @@ -17,7 +17,7 @@ ${_('Not Linked')} % endif - ${state.provider.NAME} + ${state.provider.UINAME}