From fed2722280600ff04b9966273c7a49584adca6e8 Mon Sep 17 00:00:00 2001 From: Mikko Keskinen Date: Thu, 28 Oct 2021 12:51:12 +0300 Subject: [PATCH] HP-1018 Redirect Azure AD users to AD end session view when they log out The get_ad_logout_url-method uses the "last_login_backend" value from the session or the user instance to try to find which AD the user used when they logged in. --- users/tests/test_logout_view.py | 91 +++++++++++++++++++++++++++++++++ users/views.py | 41 ++++++++++----- 2 files changed, 118 insertions(+), 14 deletions(-) diff --git a/users/tests/test_logout_view.py b/users/tests/test_logout_view.py index 9c1d2afd..a53f5e22 100644 --- a/users/tests/test_logout_view.py +++ b/users/tests/test_logout_view.py @@ -1,3 +1,4 @@ +import httpretty import pytest from django.contrib import auth from django.utils.crypto import get_random_string @@ -275,3 +276,93 @@ def test_logout_redirect_to_adfs_logout( assert response.url == expected_redirect_url else: assert response.status_code == 200 + + +@httpretty.activate(allow_net_connect=False) +@pytest.mark.django_db +def test_logout_redirect_to_azuread_logout( + settings, + client, + user, + usersocialauth_factory, + oidcclient_factory, +): + settings.AUTHENTICATION_BACKENDS = settings.AUTHENTICATION_BACKENDS + ( + 'auth_backends.helsinki_azure_ad.HelsinkiAzureADTenantOAuth2', + ) + settings.SOCIAL_AUTH_HELSINKIAZUREAD_TENANT_ID = 'fake-tenant-id' + + httpretty.register_uri( + httpretty.GET, + 'https://login.microsoftonline.com/fake-tenant-id/.well-known/openid-configuration', + body=''' + { + "end_session_endpoint": "https://example.com/end-session" + } + ''' + ) + + client.force_login(user=user) + usersocialauth_factory(provider='helsinkiazuread', user=user) + usersocialauth_factory(provider='helsinkiazuread', user=user) + + response = client.get('/openid/end-session', follow=False) + + assert response.status_code == 302 + assert response.url == 'https://example.com/end-session' + + +@httpretty.activate(allow_net_connect=False) +@pytest.mark.parametrize('last_login_backend,expected_redirect_url', ( + (None, 'https://example.com/azuread/end-session'), + ('dummy_adfs', 'https://example.com/adfs/end-session'), + ('helsinkiazuread', 'https://example.com/azuread/end-session'), +)) +@pytest.mark.parametrize('last_login_in_session', (True, False)) +@pytest.mark.django_db +def test_logout_redirect_to_last_used_ad( + settings, + client, + user, + usersocialauth_factory, + oidcclient_factory, + last_login_backend, + expected_redirect_url, + last_login_in_session, +): + settings.AUTHENTICATION_BACKENDS = settings.AUTHENTICATION_BACKENDS + ( + 'users.tests.conftest.DummyADFSBackend', + 'auth_backends.helsinki_azure_ad.HelsinkiAzureADTenantOAuth2', + ) + + DummyADFSBackend.LOGOUT_URL = 'https://example.com/adfs/end-session' + + settings.SOCIAL_AUTH_HELSINKIAZUREAD_TENANT_ID = 'fake-tenant-id' + + httpretty.register_uri( + httpretty.GET, + 'https://login.microsoftonline.com/fake-tenant-id/.well-known/openid-configuration', + body=''' + { + "end_session_endpoint": "https://example.com/azuread/end-session" + } + ''' + ) + + client.force_login(user=user) + + if last_login_in_session: + session = client.session + session['social_auth_last_login_backend'] = last_login_backend + session.save() + else: + user.last_login_backend = last_login_backend + user.save() + + usersocialauth_factory(provider='dummy_adfs', user=user) + usersocialauth_factory(provider='helsinkiazuread', user=user) + + response = client.get('/openid/end-session', follow=False) + + assert response.status_code == 302 + assert response.url == expected_redirect_url diff --git a/users/views.py b/users/views.py index 9d5aca44..cfd93659 100644 --- a/users/views.py +++ b/users/views.py @@ -5,6 +5,7 @@ from urllib.parse import parse_qs, urlencode, urlparse from django.conf import settings +from django.db.models import Case, Value, When from django.http import HttpResponse, JsonResponse from django.shortcuts import redirect from django.urls import reverse @@ -19,6 +20,7 @@ from oidc_provider.models import Client, Token from oidc_provider.views import AuthorizeView, EndSessionView, ProviderInfoView, TokenIntrospectionView, TokenView from oidc_provider.views import userinfo as oidc_provider_userinfo +from social_core.backends.azuread import AzureADOAuth2 from social_core.backends.open_id_connect import OpenIdConnectAuth from social_core.backends.utils import get_backend from social_core.exceptions import MissingBackend @@ -217,29 +219,36 @@ def _active_suomi_fi_social_user(self, user): except UserSocialAuth.DoesNotExist: return None - def get_adfs_logout_url(self, social_users): - """Returns a logout URL for an ADFS backend the user has used""" - for social_user in social_users: + def get_ad_logout_url(self, social_users, last_login_backend=None): + """Returns a logout URL for an AD backend the user has used""" + last_used_first_social_users = social_users.annotate( + is_last_used=Case( + When(provider=last_login_backend, then=Value(1)) + ) + ).order_by('is_last_used', '-modified') + + for social_user in last_used_first_social_users: try: backend_class = get_backend_class(social_user.provider) except MissingBackend: continue - if not issubclass(backend_class, BaseADFS): + if not issubclass(backend_class, (BaseADFS, AzureADOAuth2)): continue - if hasattr(backend_class, 'LOGOUT_URL') and backend_class.LOGOUT_URL: + backend = backend_class() + if hasattr(backend, 'LOGOUT_URL') and backend.LOGOUT_URL: if self.post_logout_redirect_uri: - return add_params_to_url(backend_class.LOGOUT_URL, { + return add_params_to_url(backend.LOGOUT_URL, { 'post_logout_redirect_uri': self.post_logout_redirect_uri, }) - return backend_class.LOGOUT_URL + return backend.LOGOUT_URL def get_active_social_users(self, user): if not user.is_authenticated: - return [] - return UserSocialAuth.objects.filter(user=user) + return UserSocialAuth.objects.none() + return UserSocialAuth.objects.filter(user=user).order_by('-modified') def get_oidc_backends_end_session_url(self, request, social_users): """Return end session url of the first OIDC backend the user has a @@ -293,7 +302,7 @@ def dispatch(self, request, *args, **kwargs): user = request.user social_users = self.get_active_social_users(user) - if len(social_users) > 1: + if social_users.count() > 1: logger.warning( 'Multiple active social core backends for user {}'.format( user.pk @@ -340,11 +349,15 @@ def dispatch(self, request, *args, **kwargs): self.next_page = None - # Check if the user has used an ADFS backend and redirect user + last_login_backend = request.session.get('social_auth_last_login_backend') + if not last_login_backend and hasattr(user, 'last_login_backend'): + last_login_backend = user.last_login_backend + + # Check if the user has used an AD backend and redirect user # directly to the backends logout url without showing the log out view. - adfs_logout_url = self.get_adfs_logout_url(social_users) - if adfs_logout_url: - self.next_page = adfs_logout_url + ad_logout_url = self.get_ad_logout_url(social_users, last_login_backend=last_login_backend) + if ad_logout_url: + self.next_page = ad_logout_url self.backend = None for su in social_users: