From 761a169ca2503ffdcbdf112779d731b3a3761805 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Thu, 16 Jun 2016 23:49:07 -0400 Subject: [PATCH] Added EdxOpenIdConnectLogoutView (#21) IDAs can now use this view, instead of making their own, to facilitate logouts. ECOM-2345 --- auth_backends/__init__.py | 2 +- auth_backends/backends.py | 5 +++++ auth_backends/tests/mixins.py | 23 +++++++++++++---------- auth_backends/tests/test_backends.py | 6 ++++++ auth_backends/tests/test_views.py | 22 +++++++++------------- auth_backends/urls.py | 3 ++- auth_backends/views.py | 16 ++++++++++++++++ test_settings.py | 6 ++++++ 8 files changed, 58 insertions(+), 25 deletions(-) diff --git a/auth_backends/__init__.py b/auth_backends/__init__.py index 2213689e..3edf545f 100644 --- a/auth_backends/__init__.py +++ b/auth_backends/__init__.py @@ -3,4 +3,4 @@ These package is designed to be used primarily with Open edX Django projects, but should be compatible with non-edX projects as well. """ -__version__ = '0.4.0' # pragma: no cover +__version__ = '0.5.0' # pragma: no cover diff --git a/auth_backends/backends.py b/auth_backends/backends.py index 65c07438..249119d0 100644 --- a/auth_backends/backends.py +++ b/auth_backends/backends.py @@ -55,6 +55,11 @@ def USER_INFO_URL(self): # pylint: disable=invalid-name """ URL of the auth provider's user info endpoint. """ return '{0}/user_info/'.format(self.setting('URL_ROOT')) + @property + def logout_url(self): + """ URL of the auth provider's logout page. """ + return self.setting('LOGOUT_URL') + def user_data(self, _access_token, *_args, **_kwargs): # Include decoded id_token fields in user data. return self.id_token diff --git a/auth_backends/tests/mixins.py b/auth_backends/tests/mixins.py index 8c9457e5..614bceb9 100644 --- a/auth_backends/tests/mixins.py +++ b/auth_backends/tests/mixins.py @@ -2,13 +2,21 @@ from django.contrib.auth import get_user, get_user_model from django.core.urlresolvers import reverse +from social.apps.django_app.default.models import UserSocialAuth +PASSWORD = 'test' User = get_user_model() class LogoutViewTestMixin(object): """ Mixin for tests of the LogoutRedirectBaseView children. """ + def create_user(self): + """ Create a new user. """ + user = User.objects.create_user('test', password=PASSWORD) + UserSocialAuth.objects.create(user=user, provider='edx-oidc', uid=user.username) + return user + def get_logout_url(self): """ Returns the URL of the logout view. """ return reverse('logout') @@ -22,28 +30,23 @@ def assert_authentication_status(self, is_authenticated): user = get_user(self.client) self.assertEqual(user.is_authenticated(), is_authenticated) - def test_redirect_url(self): - """ Verify the view redirects to the correct URL. """ - response = self.client.get(self.get_logout_url()) - self.assertRedirects(response, self.get_redirect_url(), fetch_redirect_response=False) - def test_x_frame_options_header(self): """ Verify no X-Frame-Options header is set in the resposne. """ response = self.client.get(self.get_logout_url()) self.assertNotIn('X-Frame-Options', response) def test_logout(self): - """ Verify the user is logged out of the current session. """ + """ Verify the user is logged out of the current session and redirected to the appropriate URL. """ self.client.logout() self.assert_authentication_status(False) - password = 'test' - user = User.objects.create_user('test', password=password) - self.client.login(username=user.username, password=password) + user = self.create_user() + self.client.login(username=user.username, password=PASSWORD) self.assert_authentication_status(True) - self.client.get(self.get_logout_url()) + response = self.client.get(self.get_logout_url()) self.assert_authentication_status(False) + self.assertRedirects(response, self.get_redirect_url(), fetch_redirect_response=False) def test_no_redirect(self): """ Verify the view does not redirect if the no_redirect querystring parameter is set. """ diff --git a/auth_backends/tests/test_backends.py b/auth_backends/tests/test_backends.py index c257367a..32c85d46 100644 --- a/auth_backends/tests/test_backends.py +++ b/auth_backends/tests/test_backends.py @@ -13,6 +13,7 @@ class EdXOpenIdConnectTests(OpenIdConnectTestMixin, OAuth2Test): backend_path = 'auth_backends.backends.EdXOpenIdConnect' url_root = 'http://www.example.com' + logout_url = 'http://www.example.com/logout/' issuer = url_root expected_username = 'test_user' fake_locale = 'en_US' @@ -28,6 +29,7 @@ def extra_settings(self): settings.update({ 'SOCIAL_AUTH_{0}_URL_ROOT'.format(self.name): self.url_root, 'SOCIAL_AUTH_{0}_ISSUER'.format(self.name): self.issuer, + 'SOCIAL_AUTH_{0}_LOGOUT_URL'.format(self.name): self.logout_url, }) return settings @@ -69,3 +71,7 @@ def test_get_user_claims(self, token_type): headers = {'Authorization': '{token_type} {token}'.format(token_type=expected_token_type, token=self.fake_access_token)} mock_get_json.assert_called_once_with(self.backend.USER_INFO_URL, headers=headers) + + def test_logout_url(self): + """ Verify the property returns the configured logout URL. """ + self.assertEqual(self.backend.logout_url, self.logout_url) diff --git a/auth_backends/tests/test_views.py b/auth_backends/tests/test_views.py index 03ff1583..8d4690f7 100644 --- a/auth_backends/tests/test_views.py +++ b/auth_backends/tests/test_views.py @@ -1,26 +1,14 @@ """ Tests for the views module. """ -from django.conf.urls import url from django.core.urlresolvers import reverse from django.test import TestCase, override_settings from auth_backends.tests.mixins import LogoutViewTestMixin from auth_backends.urls import auth_urlpatterns -from auth_backends.views import LogoutRedirectBaseView LOGOUT_REDIRECT_URL = 'https://www.example.com/logout/' -urlpatterns = auth_urlpatterns + [ - url(r'^logout/$', LogoutRedirectBaseView.as_view(url=LOGOUT_REDIRECT_URL), name='logout'), -] - - -@override_settings(ROOT_URLCONF=__name__) -class LogoutRedirectBaseViewTests(LogoutViewTestMixin, TestCase): - """ Tests for LogoutRedirectBaseView. """ - - def get_redirect_url(self): - return LOGOUT_REDIRECT_URL +urlpatterns = auth_urlpatterns @override_settings(ROOT_URLCONF=__name__) @@ -31,3 +19,11 @@ def test_redirect(self): """ Verify the view redirects to the edX OIDC login page. """ response = self.client.get(reverse('login')) self.assertRedirects(response, reverse('social:begin', args=['edx-oidc']), fetch_redirect_response=False) + + +@override_settings(ROOT_URLCONF=__name__, SOCIAL_AUTH_EDX_OIDC_LOGOUT_URL=LOGOUT_REDIRECT_URL) +class EdxOpenIdConnectLogoutView(LogoutViewTestMixin, TestCase): + """ Tests for EdxOpenIdConnectLogoutView. """ + + def get_redirect_url(self): + return LOGOUT_REDIRECT_URL diff --git a/auth_backends/urls.py b/auth_backends/urls.py index c4e0f609..c54f47b5 100644 --- a/auth_backends/urls.py +++ b/auth_backends/urls.py @@ -4,9 +4,10 @@ """ from django.conf.urls import url, include -from auth_backends.views import EdxOpenIdConnectLoginView +from auth_backends.views import EdxOpenIdConnectLoginView, EdxOpenIdConnectLogoutView auth_urlpatterns = [ # pylint: disable=invalid-name url(r'^login/$', EdxOpenIdConnectLoginView.as_view(), name='login'), + url(r'^logout/$', EdxOpenIdConnectLogoutView.as_view(), name='logout'), url('', include('social.apps.django_app.urls', namespace='social')), ] diff --git a/auth_backends/views.py b/auth_backends/views.py index ca659af9..da66f28e 100644 --- a/auth_backends/views.py +++ b/auth_backends/views.py @@ -1,4 +1,5 @@ """ Authentication views. """ +import logging from django.contrib.auth import logout from django.core.urlresolvers import reverse_lazy @@ -6,6 +7,9 @@ from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import RedirectView +from social.apps.django_app.utils import load_strategy, load_backend + +logger = logging.getLogger(__name__) class LogoutRedirectBaseView(RedirectView): @@ -24,9 +28,12 @@ class LogoutRedirectBaseView(RedirectView): authorization server's logout page. This allows signout to be triggered by the authorization server. """ permanent = False + user = None @method_decorator(xframe_options_exempt) def dispatch(self, request, *args, **kwargs): + # Keep track of the user so that child classes have access to it after logging out. + self.user = request.user logout(request) if request.GET.get('no_redirect'): @@ -43,3 +50,12 @@ class EdxOpenIdConnectLoginView(RedirectView): permanent = False query_string = True url = reverse_lazy('social:begin', args=['edx-oidc']) + + +class EdxOpenIdConnectLogoutView(LogoutRedirectBaseView): + """ Logout view for projects utilizing edX OpenID Connect for single sign-on. """ + + def get_redirect_url(self, *args, **kwargs): + strategy = load_strategy(self.request) + backend = load_backend(strategy, 'edx-oidc', None) + return backend.logout_url diff --git a/test_settings.py b/test_settings.py index 86e11554..d2be2a1a 100644 --- a/test_settings.py +++ b/test_settings.py @@ -44,6 +44,12 @@ SOCIAL_AUTH_EDX_OIDC_KEY = 'dummy-key' SOCIAL_AUTH_EDX_OIDC_SECRET = 'dummy-secret' SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY = 'dummy-secret' +SOCIAL_AUTH_EDX_OIDC_LOGOUT_URL = 'http://example.com/logout/' EXTRA_SCOPE = [] COURSE_PERMISSIONS_CLAIMS = [] + +AUTHENTICATION_BACKENDS = ( + 'auth_backends.backends.EdXOpenIdConnect', + 'django.contrib.auth.backends.ModelBackend', +)