Skip to content

Commit

Permalink
Added EdxOpenIdConnectLogoutView (#21)
Browse files Browse the repository at this point in the history
IDAs can now use this view, instead of making their own, to facilitate logouts.

ECOM-2345
  • Loading branch information
clintonb committed Jun 17, 2016
1 parent 163bc78 commit 761a169
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 25 deletions.
2 changes: 1 addition & 1 deletion auth_backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions auth_backends/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 13 additions & 10 deletions auth_backends/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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. """
Expand Down
6 changes: 6 additions & 0 deletions auth_backends/tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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

Expand Down Expand Up @@ -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)
22 changes: 9 additions & 13 deletions auth_backends/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand All @@ -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
3 changes: 2 additions & 1 deletion auth_backends/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
]
16 changes: 16 additions & 0 deletions auth_backends/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
""" Authentication views. """
import logging

from django.contrib.auth import logout
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse
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):
Expand All @@ -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'):
Expand All @@ -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
6 changes: 6 additions & 0 deletions test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)

0 comments on commit 761a169

Please sign in to comment.