authentik.providers.saml.views.sp_slo

SP-initiated SAML Single Logout Views

  1"""SP-initiated SAML Single Logout Views"""
  2
  3from django.http import Http404, HttpRequest, HttpResponse
  4from django.shortcuts import get_object_or_404, redirect
  5from django.utils.decorators import method_decorator
  6from django.views.decorators.clickjacking import xframe_options_sameorigin
  7from django.views.decorators.csrf import csrf_exempt
  8from structlog.stdlib import get_logger
  9
 10from authentik.core.models import Application, AuthenticatedSession
 11from authentik.events.models import Event, EventAction
 12from authentik.flows.models import Flow, in_memory_stage
 13from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan, FlowPlanner
 14from authentik.flows.stage import SessionEndStage
 15from authentik.flows.views.executor import SESSION_KEY_PLAN
 16from authentik.lib.views import bad_request_message
 17from authentik.policies.views import PolicyAccessView
 18from authentik.providers.iframe_logout import IframeLogoutStageView
 19from authentik.providers.saml.exceptions import CannotHandleAssertion
 20from authentik.providers.saml.models import (
 21    SAMLBindings,
 22    SAMLLogoutMethods,
 23    SAMLProvider,
 24    SAMLSession,
 25)
 26from authentik.providers.saml.native_logout import NativeLogoutStageView
 27from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
 28from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
 29from authentik.providers.saml.tasks import send_saml_logout_response
 30from authentik.providers.saml.utils.encoding import nice64
 31from authentik.providers.saml.views.flows import (
 32    PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS,
 33    PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS,
 34    PLAN_CONTEXT_SAML_LOGOUT_REQUEST,
 35    PLAN_CONTEXT_SAML_RELAY_STATE,
 36    REQUEST_KEY_RELAY_STATE,
 37    REQUEST_KEY_SAML_REQUEST,
 38    REQUEST_KEY_SAML_RESPONSE,
 39)
 40
 41LOGGER = get_logger()
 42
 43
 44def _get_redirect_url(request: HttpRequest, relay_state: str = "") -> str:
 45    """Get the safe redirect URL from the plan context, logging a warning if the
 46    incoming relay_state doesn't match the stored value."""
 47    stored_relay_state = ""
 48    if SESSION_KEY_PLAN in request.session:
 49        plan: FlowPlan = request.session[SESSION_KEY_PLAN]
 50        stored_relay_state = plan.context.get(PLAN_CONTEXT_SAML_RELAY_STATE, "")
 51
 52    if relay_state and relay_state != stored_relay_state:
 53        LOGGER.warning(
 54            "SAML logout relay_state mismatch, possible open redirect attempt",
 55            received_relay_state=relay_state,
 56            stored_relay_state=stored_relay_state,
 57        )
 58
 59    return stored_relay_state
 60
 61
 62class SPInitiatedSLOView(PolicyAccessView):
 63    """Handle SP-initiated SAML Single Logout requests"""
 64
 65    flow: Flow
 66
 67    def __init__(self, **kwargs):
 68        super().__init__(**kwargs)
 69        self.plan_context = {}
 70
 71    def resolve_provider_application(self):
 72        self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
 73        self.provider: SAMLProvider = get_object_or_404(
 74            SAMLProvider, pk=self.application.provider_id
 75        )
 76        self.flow = self.provider.invalidation_flow or self.request.brand.flow_invalidation
 77        if not self.flow:
 78            raise Http404
 79
 80    def check_saml_request(self) -> HttpRequest | None:
 81        """Handler to verify the SAML Request. Must be implemented by a subclass"""
 82        raise NotImplementedError
 83
 84    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
 85        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
 86
 87        # Call the method handler, which checks the SAML
 88        # Request and returns a HTTP Response on error
 89        method_response = self.check_saml_request()
 90        if method_response:
 91            return method_response
 92        planner = FlowPlanner(self.flow)
 93        planner.allow_empty_flows = True
 94        plan = planner.plan(
 95            request,
 96            {
 97                PLAN_CONTEXT_APPLICATION: self.application,
 98                **self.plan_context,
 99            },
100        )
101
102        if self.provider.sls_url:
103            # Get logout request and extract relay state
104            logout_request = self.plan_context.get(PLAN_CONTEXT_SAML_LOGOUT_REQUEST)
105            relay_state = logout_request.relay_state if logout_request else None
106
107            # Store relay state for the logout response
108            plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
109
110            # Look up the session issuer to use in the logout response
111            auth_session = AuthenticatedSession.from_request(request, request.user)
112            session_issuer = None
113            if auth_session:
114                saml_session = SAMLSession.objects.filter(
115                    session=auth_session,
116                    user=request.user,
117                    provider=self.provider,
118                ).first()
119                if saml_session:
120                    session_issuer = saml_session.issuer
121
122            if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
123                # Native mode - user will be redirected/posted away from authentik
124                processor = LogoutResponseProcessor(
125                    self.provider,
126                    logout_request,
127                    destination=self.provider.sls_url,
128                    issuer=session_issuer,
129                )
130
131                if self.provider.sls_binding == SAMLBindings.POST:
132                    logout_response = processor.encode_post()
133                    logout_data = {
134                        "post_url": self.provider.sls_url,
135                        "saml_response": logout_response,
136                        "saml_relay_state": relay_state,
137                        "provider_name": self.provider.name,
138                        "saml_binding": SAMLBindings.POST,
139                    }
140                else:
141                    logout_url = processor.get_redirect_url()
142                    logout_data = {
143                        "redirect_url": logout_url,
144                        "provider_name": self.provider.name,
145                        "saml_binding": SAMLBindings.REDIRECT,
146                    }
147
148                plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
149                plan.append_stage(in_memory_stage(NativeLogoutStageView))
150            elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
151                # Backchannel mode - server sends logout response directly to SP in background
152                # No user interaction needed
153                if self.provider.sls_binding != SAMLBindings.POST:
154                    LOGGER.warning(
155                        "Backchannel logout requires POST binding, but provider is configured "
156                        "with %s binding",
157                        self.provider.sls_binding,
158                        provider=self.provider,
159                    )
160
161                # Queue the logout response to be sent in the background
162                # This doesn't block the user's logout from completing
163                send_saml_logout_response.send(
164                    provider_pk=self.provider.pk,
165                    sls_url=self.provider.sls_url,
166                    logout_request_id=logout_request.id if logout_request else None,
167                    relay_state=relay_state,
168                    issuer=session_issuer,
169                )
170
171                LOGGER.debug(
172                    "Queued backchannel logout response",
173                    provider=self.provider,
174                    sls_url=self.provider.sls_url,
175                )
176
177                # Just end the session - no user interaction needed
178                plan.append_stage(in_memory_stage(SessionEndStage))
179            else:
180                # Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
181                processor = LogoutResponseProcessor(
182                    self.provider,
183                    logout_request,
184                    destination=self.provider.sls_url,
185                    issuer=session_issuer,
186                )
187
188                logout_response = processor.build_response()
189
190                if self.provider.sls_binding == SAMLBindings.POST:
191                    logout_data = {
192                        "url": self.provider.sls_url,
193                        "saml_response": nice64(logout_response),
194                        "saml_relay_state": relay_state,
195                        "provider_name": self.provider.name,
196                        "binding": SAMLBindings.POST,
197                    }
198                else:
199                    logout_url = processor.get_redirect_url()
200                    logout_data = {
201                        "url": logout_url,
202                        "provider_name": self.provider.name,
203                        "binding": SAMLBindings.REDIRECT,
204                    }
205
206                plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
207                plan.append_stage(in_memory_stage(IframeLogoutStageView))
208                plan.append_stage(in_memory_stage(SessionEndStage))
209        else:
210            # No SLS URL configured, just end session
211            plan.append_stage(in_memory_stage(SessionEndStage))
212
213        # Remove samlsession from database
214        auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
215        if auth_session:
216            SAMLSession.objects.filter(
217                session=auth_session,
218                user=self.request.user,
219                provider=self.provider,
220            ).delete()
221        return plan.to_redirect(self.request, self.flow)
222
223    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
224        """GET and POST use the same handler, but we can't
225        override .dispatch easily because PolicyAccessView's dispatch"""
226        return self.get(request, application_slug)
227
228
229class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
230    """SAML Handler for SP initiated SLO/Redirect bindings, which are sent via GET"""
231
232    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
233        """Override dispatch to handle logout responses before authentication check"""
234        # Check if this is a LogoutResponse before doing any authentication checks
235        # If we receive a logoutResponse, this means we are using native redirect
236        # IDP SLO, so we want to redirect to our next provider
237        if REQUEST_KEY_SAML_RESPONSE in request.GET:
238            relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
239            redirect_url = _get_redirect_url(request, relay_state)
240            if redirect_url:
241                return redirect(redirect_url)
242            return redirect("authentik_core:root-redirect")
243
244        # For SAML logout requests, use the parent dispatch with auth checks
245        return super().dispatch(request, *args, **kwargs)
246
247    def check_saml_request(self) -> HttpRequest | None:
248        # Logout responses are now handled in dispatch()
249        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
250            LOGGER.info("check_saml_request: SAML payload missing")
251            return bad_request_message(self.request, "The SAML request payload is missing.")
252
253        try:
254            logout_request = LogoutRequestParser(self.provider).parse_detached(
255                self.request.GET[REQUEST_KEY_SAML_REQUEST],
256                relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
257            )
258            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
259        except CannotHandleAssertion as exc:
260            Event.new(
261                EventAction.CONFIGURATION_ERROR,
262                provider=self.provider,
263                message=str(exc),
264            ).save()
265            LOGGER.info(str(exc))
266            return bad_request_message(self.request, str(exc))
267        return None
268
269
270@method_decorator(xframe_options_sameorigin, name="dispatch")
271@method_decorator(csrf_exempt, name="dispatch")
272class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
273    """SAML Handler for SP-initiated SLO with POST binding"""
274
275    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
276        """Override dispatch to handle logout requests and responses"""
277        # Check if this is a LogoutResponse before doing any authentication checks
278        # If we receive a logoutResponse, this means we are using native redirect
279        # IDP SLO, so we want to redirect to our next provider
280        if REQUEST_KEY_SAML_RESPONSE in request.POST:
281            relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
282            redirect_url = _get_redirect_url(request, relay_state)
283            if redirect_url:
284                return redirect(redirect_url)
285            return redirect("authentik_core:root-redirect")
286
287        # For SAML logout requests, use the parent dispatch with auth checks
288        return super().dispatch(request, *args, **kwargs)
289
290    def check_saml_request(self) -> HttpRequest | None:
291        payload = self.request.POST
292        # Logout responses are now handled in dispatch()
293        if REQUEST_KEY_SAML_REQUEST not in payload:
294            LOGGER.info("check_saml_request: SAML payload missing")
295            return bad_request_message(self.request, "The SAML request payload is missing.")
296
297        try:
298            logout_request = LogoutRequestParser(self.provider).parse(
299                payload[REQUEST_KEY_SAML_REQUEST],
300                relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
301            )
302            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
303        except CannotHandleAssertion as exc:
304            LOGGER.info(str(exc))
305            return bad_request_message(self.request, str(exc))
306        return None
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class SPInitiatedSLOView(authentik.policies.views.PolicyAccessView):
 63class SPInitiatedSLOView(PolicyAccessView):
 64    """Handle SP-initiated SAML Single Logout requests"""
 65
 66    flow: Flow
 67
 68    def __init__(self, **kwargs):
 69        super().__init__(**kwargs)
 70        self.plan_context = {}
 71
 72    def resolve_provider_application(self):
 73        self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
 74        self.provider: SAMLProvider = get_object_or_404(
 75            SAMLProvider, pk=self.application.provider_id
 76        )
 77        self.flow = self.provider.invalidation_flow or self.request.brand.flow_invalidation
 78        if not self.flow:
 79            raise Http404
 80
 81    def check_saml_request(self) -> HttpRequest | None:
 82        """Handler to verify the SAML Request. Must be implemented by a subclass"""
 83        raise NotImplementedError
 84
 85    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
 86        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
 87
 88        # Call the method handler, which checks the SAML
 89        # Request and returns a HTTP Response on error
 90        method_response = self.check_saml_request()
 91        if method_response:
 92            return method_response
 93        planner = FlowPlanner(self.flow)
 94        planner.allow_empty_flows = True
 95        plan = planner.plan(
 96            request,
 97            {
 98                PLAN_CONTEXT_APPLICATION: self.application,
 99                **self.plan_context,
100            },
101        )
102
103        if self.provider.sls_url:
104            # Get logout request and extract relay state
105            logout_request = self.plan_context.get(PLAN_CONTEXT_SAML_LOGOUT_REQUEST)
106            relay_state = logout_request.relay_state if logout_request else None
107
108            # Store relay state for the logout response
109            plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
110
111            # Look up the session issuer to use in the logout response
112            auth_session = AuthenticatedSession.from_request(request, request.user)
113            session_issuer = None
114            if auth_session:
115                saml_session = SAMLSession.objects.filter(
116                    session=auth_session,
117                    user=request.user,
118                    provider=self.provider,
119                ).first()
120                if saml_session:
121                    session_issuer = saml_session.issuer
122
123            if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
124                # Native mode - user will be redirected/posted away from authentik
125                processor = LogoutResponseProcessor(
126                    self.provider,
127                    logout_request,
128                    destination=self.provider.sls_url,
129                    issuer=session_issuer,
130                )
131
132                if self.provider.sls_binding == SAMLBindings.POST:
133                    logout_response = processor.encode_post()
134                    logout_data = {
135                        "post_url": self.provider.sls_url,
136                        "saml_response": logout_response,
137                        "saml_relay_state": relay_state,
138                        "provider_name": self.provider.name,
139                        "saml_binding": SAMLBindings.POST,
140                    }
141                else:
142                    logout_url = processor.get_redirect_url()
143                    logout_data = {
144                        "redirect_url": logout_url,
145                        "provider_name": self.provider.name,
146                        "saml_binding": SAMLBindings.REDIRECT,
147                    }
148
149                plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
150                plan.append_stage(in_memory_stage(NativeLogoutStageView))
151            elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
152                # Backchannel mode - server sends logout response directly to SP in background
153                # No user interaction needed
154                if self.provider.sls_binding != SAMLBindings.POST:
155                    LOGGER.warning(
156                        "Backchannel logout requires POST binding, but provider is configured "
157                        "with %s binding",
158                        self.provider.sls_binding,
159                        provider=self.provider,
160                    )
161
162                # Queue the logout response to be sent in the background
163                # This doesn't block the user's logout from completing
164                send_saml_logout_response.send(
165                    provider_pk=self.provider.pk,
166                    sls_url=self.provider.sls_url,
167                    logout_request_id=logout_request.id if logout_request else None,
168                    relay_state=relay_state,
169                    issuer=session_issuer,
170                )
171
172                LOGGER.debug(
173                    "Queued backchannel logout response",
174                    provider=self.provider,
175                    sls_url=self.provider.sls_url,
176                )
177
178                # Just end the session - no user interaction needed
179                plan.append_stage(in_memory_stage(SessionEndStage))
180            else:
181                # Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
182                processor = LogoutResponseProcessor(
183                    self.provider,
184                    logout_request,
185                    destination=self.provider.sls_url,
186                    issuer=session_issuer,
187                )
188
189                logout_response = processor.build_response()
190
191                if self.provider.sls_binding == SAMLBindings.POST:
192                    logout_data = {
193                        "url": self.provider.sls_url,
194                        "saml_response": nice64(logout_response),
195                        "saml_relay_state": relay_state,
196                        "provider_name": self.provider.name,
197                        "binding": SAMLBindings.POST,
198                    }
199                else:
200                    logout_url = processor.get_redirect_url()
201                    logout_data = {
202                        "url": logout_url,
203                        "provider_name": self.provider.name,
204                        "binding": SAMLBindings.REDIRECT,
205                    }
206
207                plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
208                plan.append_stage(in_memory_stage(IframeLogoutStageView))
209                plan.append_stage(in_memory_stage(SessionEndStage))
210        else:
211            # No SLS URL configured, just end session
212            plan.append_stage(in_memory_stage(SessionEndStage))
213
214        # Remove samlsession from database
215        auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
216        if auth_session:
217            SAMLSession.objects.filter(
218                session=auth_session,
219                user=self.request.user,
220                provider=self.provider,
221            ).delete()
222        return plan.to_redirect(self.request, self.flow)
223
224    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
225        """GET and POST use the same handler, but we can't
226        override .dispatch easily because PolicyAccessView's dispatch"""
227        return self.get(request, application_slug)

Handle SP-initiated SAML Single Logout requests

SPInitiatedSLOView(**kwargs)
68    def __init__(self, **kwargs):
69        super().__init__(**kwargs)
70        self.plan_context = {}

Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.

plan_context
def resolve_provider_application(self):
72    def resolve_provider_application(self):
73        self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
74        self.provider: SAMLProvider = get_object_or_404(
75            SAMLProvider, pk=self.application.provider_id
76        )
77        self.flow = self.provider.invalidation_flow or self.request.brand.flow_invalidation
78        if not self.flow:
79            raise Http404

Resolve self.provider and self.application. *.DoesNotExist Exceptions cause a normal AccessDenied view to be shown. An Http404 exception is not caught, and will return directly

def check_saml_request(self) -> django.http.request.HttpRequest | None:
81    def check_saml_request(self) -> HttpRequest | None:
82        """Handler to verify the SAML Request. Must be implemented by a subclass"""
83        raise NotImplementedError

Handler to verify the SAML Request. Must be implemented by a subclass

def get( self, request: django.http.request.HttpRequest, application_slug: str) -> django.http.response.HttpResponse:
 85    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
 86        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
 87
 88        # Call the method handler, which checks the SAML
 89        # Request and returns a HTTP Response on error
 90        method_response = self.check_saml_request()
 91        if method_response:
 92            return method_response
 93        planner = FlowPlanner(self.flow)
 94        planner.allow_empty_flows = True
 95        plan = planner.plan(
 96            request,
 97            {
 98                PLAN_CONTEXT_APPLICATION: self.application,
 99                **self.plan_context,
100            },
101        )
102
103        if self.provider.sls_url:
104            # Get logout request and extract relay state
105            logout_request = self.plan_context.get(PLAN_CONTEXT_SAML_LOGOUT_REQUEST)
106            relay_state = logout_request.relay_state if logout_request else None
107
108            # Store relay state for the logout response
109            plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
110
111            # Look up the session issuer to use in the logout response
112            auth_session = AuthenticatedSession.from_request(request, request.user)
113            session_issuer = None
114            if auth_session:
115                saml_session = SAMLSession.objects.filter(
116                    session=auth_session,
117                    user=request.user,
118                    provider=self.provider,
119                ).first()
120                if saml_session:
121                    session_issuer = saml_session.issuer
122
123            if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
124                # Native mode - user will be redirected/posted away from authentik
125                processor = LogoutResponseProcessor(
126                    self.provider,
127                    logout_request,
128                    destination=self.provider.sls_url,
129                    issuer=session_issuer,
130                )
131
132                if self.provider.sls_binding == SAMLBindings.POST:
133                    logout_response = processor.encode_post()
134                    logout_data = {
135                        "post_url": self.provider.sls_url,
136                        "saml_response": logout_response,
137                        "saml_relay_state": relay_state,
138                        "provider_name": self.provider.name,
139                        "saml_binding": SAMLBindings.POST,
140                    }
141                else:
142                    logout_url = processor.get_redirect_url()
143                    logout_data = {
144                        "redirect_url": logout_url,
145                        "provider_name": self.provider.name,
146                        "saml_binding": SAMLBindings.REDIRECT,
147                    }
148
149                plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
150                plan.append_stage(in_memory_stage(NativeLogoutStageView))
151            elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
152                # Backchannel mode - server sends logout response directly to SP in background
153                # No user interaction needed
154                if self.provider.sls_binding != SAMLBindings.POST:
155                    LOGGER.warning(
156                        "Backchannel logout requires POST binding, but provider is configured "
157                        "with %s binding",
158                        self.provider.sls_binding,
159                        provider=self.provider,
160                    )
161
162                # Queue the logout response to be sent in the background
163                # This doesn't block the user's logout from completing
164                send_saml_logout_response.send(
165                    provider_pk=self.provider.pk,
166                    sls_url=self.provider.sls_url,
167                    logout_request_id=logout_request.id if logout_request else None,
168                    relay_state=relay_state,
169                    issuer=session_issuer,
170                )
171
172                LOGGER.debug(
173                    "Queued backchannel logout response",
174                    provider=self.provider,
175                    sls_url=self.provider.sls_url,
176                )
177
178                # Just end the session - no user interaction needed
179                plan.append_stage(in_memory_stage(SessionEndStage))
180            else:
181                # Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
182                processor = LogoutResponseProcessor(
183                    self.provider,
184                    logout_request,
185                    destination=self.provider.sls_url,
186                    issuer=session_issuer,
187                )
188
189                logout_response = processor.build_response()
190
191                if self.provider.sls_binding == SAMLBindings.POST:
192                    logout_data = {
193                        "url": self.provider.sls_url,
194                        "saml_response": nice64(logout_response),
195                        "saml_relay_state": relay_state,
196                        "provider_name": self.provider.name,
197                        "binding": SAMLBindings.POST,
198                    }
199                else:
200                    logout_url = processor.get_redirect_url()
201                    logout_data = {
202                        "url": logout_url,
203                        "provider_name": self.provider.name,
204                        "binding": SAMLBindings.REDIRECT,
205                    }
206
207                plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
208                plan.append_stage(in_memory_stage(IframeLogoutStageView))
209                plan.append_stage(in_memory_stage(SessionEndStage))
210        else:
211            # No SLS URL configured, just end session
212            plan.append_stage(in_memory_stage(SessionEndStage))
213
214        # Remove samlsession from database
215        auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
216        if auth_session:
217            SAMLSession.objects.filter(
218                session=auth_session,
219                user=self.request.user,
220                provider=self.provider,
221            ).delete()
222        return plan.to_redirect(self.request, self.flow)

Verify the SAML Request, and if valid initiate the FlowPlanner for the application

def post( self, request: django.http.request.HttpRequest, application_slug: str) -> django.http.response.HttpResponse:
224    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
225        """GET and POST use the same handler, but we can't
226        override .dispatch easily because PolicyAccessView's dispatch"""
227        return self.get(request, application_slug)

GET and POST use the same handler, but we can't override .dispatch easily because PolicyAccessView's dispatch

class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
230class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
231    """SAML Handler for SP initiated SLO/Redirect bindings, which are sent via GET"""
232
233    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
234        """Override dispatch to handle logout responses before authentication check"""
235        # Check if this is a LogoutResponse before doing any authentication checks
236        # If we receive a logoutResponse, this means we are using native redirect
237        # IDP SLO, so we want to redirect to our next provider
238        if REQUEST_KEY_SAML_RESPONSE in request.GET:
239            relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
240            redirect_url = _get_redirect_url(request, relay_state)
241            if redirect_url:
242                return redirect(redirect_url)
243            return redirect("authentik_core:root-redirect")
244
245        # For SAML logout requests, use the parent dispatch with auth checks
246        return super().dispatch(request, *args, **kwargs)
247
248    def check_saml_request(self) -> HttpRequest | None:
249        # Logout responses are now handled in dispatch()
250        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
251            LOGGER.info("check_saml_request: SAML payload missing")
252            return bad_request_message(self.request, "The SAML request payload is missing.")
253
254        try:
255            logout_request = LogoutRequestParser(self.provider).parse_detached(
256                self.request.GET[REQUEST_KEY_SAML_REQUEST],
257                relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
258            )
259            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
260        except CannotHandleAssertion as exc:
261            Event.new(
262                EventAction.CONFIGURATION_ERROR,
263                provider=self.provider,
264                message=str(exc),
265            ).save()
266            LOGGER.info(str(exc))
267            return bad_request_message(self.request, str(exc))
268        return None

SAML Handler for SP initiated SLO/Redirect bindings, which are sent via GET

def dispatch( self, request: django.http.request.HttpRequest, *args, **kwargs) -> django.http.response.HttpResponse:
233    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
234        """Override dispatch to handle logout responses before authentication check"""
235        # Check if this is a LogoutResponse before doing any authentication checks
236        # If we receive a logoutResponse, this means we are using native redirect
237        # IDP SLO, so we want to redirect to our next provider
238        if REQUEST_KEY_SAML_RESPONSE in request.GET:
239            relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
240            redirect_url = _get_redirect_url(request, relay_state)
241            if redirect_url:
242                return redirect(redirect_url)
243            return redirect("authentik_core:root-redirect")
244
245        # For SAML logout requests, use the parent dispatch with auth checks
246        return super().dispatch(request, *args, **kwargs)

Override dispatch to handle logout responses before authentication check

def check_saml_request(self) -> django.http.request.HttpRequest | None:
248    def check_saml_request(self) -> HttpRequest | None:
249        # Logout responses are now handled in dispatch()
250        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
251            LOGGER.info("check_saml_request: SAML payload missing")
252            return bad_request_message(self.request, "The SAML request payload is missing.")
253
254        try:
255            logout_request = LogoutRequestParser(self.provider).parse_detached(
256                self.request.GET[REQUEST_KEY_SAML_REQUEST],
257                relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
258            )
259            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
260        except CannotHandleAssertion as exc:
261            Event.new(
262                EventAction.CONFIGURATION_ERROR,
263                provider=self.provider,
264                message=str(exc),
265            ).save()
266            LOGGER.info(str(exc))
267            return bad_request_message(self.request, str(exc))
268        return None

Handler to verify the SAML Request. Must be implemented by a subclass

@method_decorator(xframe_options_sameorigin, name='dispatch')
@method_decorator(csrf_exempt, name='dispatch')
class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
271@method_decorator(xframe_options_sameorigin, name="dispatch")
272@method_decorator(csrf_exempt, name="dispatch")
273class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
274    """SAML Handler for SP-initiated SLO with POST binding"""
275
276    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
277        """Override dispatch to handle logout requests and responses"""
278        # Check if this is a LogoutResponse before doing any authentication checks
279        # If we receive a logoutResponse, this means we are using native redirect
280        # IDP SLO, so we want to redirect to our next provider
281        if REQUEST_KEY_SAML_RESPONSE in request.POST:
282            relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
283            redirect_url = _get_redirect_url(request, relay_state)
284            if redirect_url:
285                return redirect(redirect_url)
286            return redirect("authentik_core:root-redirect")
287
288        # For SAML logout requests, use the parent dispatch with auth checks
289        return super().dispatch(request, *args, **kwargs)
290
291    def check_saml_request(self) -> HttpRequest | None:
292        payload = self.request.POST
293        # Logout responses are now handled in dispatch()
294        if REQUEST_KEY_SAML_REQUEST not in payload:
295            LOGGER.info("check_saml_request: SAML payload missing")
296            return bad_request_message(self.request, "The SAML request payload is missing.")
297
298        try:
299            logout_request = LogoutRequestParser(self.provider).parse(
300                payload[REQUEST_KEY_SAML_REQUEST],
301                relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
302            )
303            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
304        except CannotHandleAssertion as exc:
305            LOGGER.info(str(exc))
306            return bad_request_message(self.request, str(exc))
307        return None

SAML Handler for SP-initiated SLO with POST binding

def dispatch( self, request: django.http.request.HttpRequest, *args, **kwargs) -> django.http.response.HttpResponse:
276    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
277        """Override dispatch to handle logout requests and responses"""
278        # Check if this is a LogoutResponse before doing any authentication checks
279        # If we receive a logoutResponse, this means we are using native redirect
280        # IDP SLO, so we want to redirect to our next provider
281        if REQUEST_KEY_SAML_RESPONSE in request.POST:
282            relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
283            redirect_url = _get_redirect_url(request, relay_state)
284            if redirect_url:
285                return redirect(redirect_url)
286            return redirect("authentik_core:root-redirect")
287
288        # For SAML logout requests, use the parent dispatch with auth checks
289        return super().dispatch(request, *args, **kwargs)

Override dispatch to handle logout requests and responses

def check_saml_request(self) -> django.http.request.HttpRequest | None:
291    def check_saml_request(self) -> HttpRequest | None:
292        payload = self.request.POST
293        # Logout responses are now handled in dispatch()
294        if REQUEST_KEY_SAML_REQUEST not in payload:
295            LOGGER.info("check_saml_request: SAML payload missing")
296            return bad_request_message(self.request, "The SAML request payload is missing.")
297
298        try:
299            logout_request = LogoutRequestParser(self.provider).parse(
300                payload[REQUEST_KEY_SAML_REQUEST],
301                relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
302            )
303            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
304        except CannotHandleAssertion as exc:
305            LOGGER.info(str(exc))
306            return bad_request_message(self.request, str(exc))
307        return None

Handler to verify the SAML Request. Must be implemented by a subclass