Skip to content

Commit

Permalink
HP-1018 Redirect Azure AD users to AD end session view when they log out
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mikkokeskinen authored and tituomin committed Nov 9, 2021
1 parent e4b5857 commit fed2722
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 14 deletions.
91 changes: 91 additions & 0 deletions users/tests/test_logout_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import httpretty
import pytest
from django.contrib import auth
from django.utils.crypto import get_random_string
Expand Down Expand Up @@ -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
41 changes: 27 additions & 14 deletions users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit fed2722

Please sign in to comment.