From b9592384424b204fc3a98e28dd152258c07beff3 Mon Sep 17 00:00:00 2001 From: Danny Lee Date: Tue, 23 Jul 2024 02:48:14 -0700 Subject: [PATCH 1/2] feat(saml2): Implement SP-initiated Single Logout --- src/sentry/api/endpoints/auth_index.py | 3 +++ src/sentry/auth/providers/saml2/provider.py | 26 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/endpoints/auth_index.py b/src/sentry/api/endpoints/auth_index.py index ec3b5a2abd36e8..725d0a55feaee3 100644 --- a/src/sentry/api/endpoints/auth_index.py +++ b/src/sentry/api/endpoints/auth_index.py @@ -18,6 +18,7 @@ from sentry.api.validators import AuthVerifyValidator from sentry.api.validators.auth import MISSING_PASSWORD_OR_U2F_CODE from sentry.auth.authenticators.u2f import U2fInterface +from sentry.auth.providers.saml2.provider import handle_saml_single_logout from sentry.auth.services.auth.impl import promote_request_rpc_user from sentry.auth.superuser import SUPERUSER_ORG_ID from sentry.models.authenticator import Authenticator @@ -293,6 +294,8 @@ def delete(self, request: Request, *args, **kwargs) -> Response: Deauthenticate all active sessions for this user. """ + handle_saml_single_logout(request) + # For signals to work here, we must promote the request.user to a full user object logout(request._request) request.user = AnonymousUser() diff --git a/src/sentry/auth/providers/saml2/provider.py b/src/sentry/auth/providers/saml2/provider.py index d836ea4742c2db..80eb03630717dc 100644 --- a/src/sentry/auth/providers/saml2/provider.py +++ b/src/sentry/auth/providers/saml2/provider.py @@ -1,6 +1,7 @@ import abc from urllib.parse import urlparse +import sentry_sdk from django.contrib import messages from django.contrib.auth import logout from django.http import HttpResponse, HttpResponseServerError @@ -17,7 +18,7 @@ from sentry.auth.provider import Provider from sentry.auth.view import AuthView from sentry.models.authprovider import AuthProvider -from sentry.models.organization import OrganizationStatus +from sentry.models.organization import Organization, OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.organizations.services.organization import organization_service from sentry.utils.auth import get_login_url @@ -387,3 +388,26 @@ def build_auth(request, saml_config): } return OneLogin_Saml2_Auth(saml_request, saml_config) + + +def handle_saml_single_logout(request): + # Do not handle SLO if a user is in more than 1 organization + # Propagating it to multiple IdPs results in confusion for the user + organizations = Organization.objects.get_for_user(user=request.user) + if not len(organizations) == 1: + return + + org = organizations[0] + provider = get_provider(org.slug) + + if not provider or not provider.is_saml: + return + + # Try/catch is needed because IdP may not support SLO (e.g. Okta) and + # will return an error + try: + saml_config = build_saml_config(provider.config, org) + idp_auth = build_auth(request, saml_config) + idp_auth.logout() + except Exception as e: + sentry_sdk.capture_exception(e) From 90b3c3cae29095bf855a9a0ecfd9b39061506e89 Mon Sep 17 00:00:00 2001 From: Danny Lee Date: Tue, 23 Jul 2024 09:55:44 -0700 Subject: [PATCH 2/2] chore(saml2): Add flag for method --- src/sentry/auth/providers/saml2/provider.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sentry/auth/providers/saml2/provider.py b/src/sentry/auth/providers/saml2/provider.py index 80eb03630717dc..41ef66f4d171fe 100644 --- a/src/sentry/auth/providers/saml2/provider.py +++ b/src/sentry/auth/providers/saml2/provider.py @@ -13,14 +13,15 @@ from onelogin.saml2.constants import OneLogin_Saml2_Constants from rest_framework.request import Request -from sentry import options +from sentry import features, options from sentry.auth.exceptions import IdentityNotValid from sentry.auth.provider import Provider from sentry.auth.view import AuthView from sentry.models.authprovider import AuthProvider -from sentry.models.organization import Organization, OrganizationStatus +from sentry.models.organization import OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.organizations.services.organization import organization_service +from sentry.users.services.user.service import user_service from sentry.utils.auth import get_login_url from sentry.utils.http import absolute_uri from sentry.web.frontend.base import BaseView, control_silo_view @@ -393,13 +394,15 @@ def build_auth(request, saml_config): def handle_saml_single_logout(request): # Do not handle SLO if a user is in more than 1 organization # Propagating it to multiple IdPs results in confusion for the user - organizations = Organization.objects.get_for_user(user=request.user) + organizations = user_service.get_organizations(user_id=request.user.id) if not len(organizations) == 1: return org = organizations[0] - provider = get_provider(org.slug) + if not features.has("organizations:sso-saml2-slo", org): + return + provider = get_provider(org.slug) if not provider or not provider.is_saml: return