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            if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
111                # Native mode - user will be redirected/posted away from authentik
112                processor = LogoutResponseProcessor(
113                    self.provider,
114                    logout_request,
115                    destination=self.provider.sls_url,
116                )
117
118                if self.provider.sls_binding == SAMLBindings.POST:
119                    logout_response = processor.encode_post()
120                    logout_data = {
121                        "post_url": self.provider.sls_url,
122                        "saml_response": logout_response,
123                        "saml_relay_state": relay_state,
124                        "provider_name": self.provider.name,
125                        "saml_binding": SAMLBindings.POST,
126                    }
127                else:
128                    logout_url = processor.get_redirect_url()
129                    logout_data = {
130                        "redirect_url": logout_url,
131                        "provider_name": self.provider.name,
132                        "saml_binding": SAMLBindings.REDIRECT,
133                    }
134
135                plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
136                plan.append_stage(in_memory_stage(NativeLogoutStageView))
137            elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
138                # Backchannel mode - server sends logout response directly to SP in background
139                # No user interaction needed
140                if self.provider.sls_binding != SAMLBindings.POST:
141                    LOGGER.warning(
142                        "Backchannel logout requires POST binding, but provider is configured "
143                        "with %s binding",
144                        self.provider.sls_binding,
145                        provider=self.provider,
146                    )
147
148                # Queue the logout response to be sent in the background
149                # This doesn't block the user's logout from completing
150                send_saml_logout_response.send(
151                    provider_pk=self.provider.pk,
152                    sls_url=self.provider.sls_url,
153                    logout_request_id=logout_request.id if logout_request else None,
154                    relay_state=relay_state,
155                )
156
157                LOGGER.debug(
158                    "Queued backchannel logout response",
159                    provider=self.provider,
160                    sls_url=self.provider.sls_url,
161                )
162
163                # Just end the session - no user interaction needed
164                plan.append_stage(in_memory_stage(SessionEndStage))
165            else:
166                # Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
167                processor = LogoutResponseProcessor(
168                    self.provider,
169                    logout_request,
170                    destination=self.provider.sls_url,
171                )
172
173                logout_response = processor.build_response()
174
175                if self.provider.sls_binding == SAMLBindings.POST:
176                    logout_data = {
177                        "url": self.provider.sls_url,
178                        "saml_response": nice64(logout_response),
179                        "saml_relay_state": relay_state,
180                        "provider_name": self.provider.name,
181                        "binding": SAMLBindings.POST,
182                    }
183                else:
184                    logout_url = processor.get_redirect_url()
185                    logout_data = {
186                        "url": logout_url,
187                        "provider_name": self.provider.name,
188                        "binding": SAMLBindings.REDIRECT,
189                    }
190
191                plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
192                plan.append_stage(in_memory_stage(IframeLogoutStageView))
193                plan.append_stage(in_memory_stage(SessionEndStage))
194        else:
195            # No SLS URL configured, just end session
196            plan.append_stage(in_memory_stage(SessionEndStage))
197
198        # Remove samlsession from database
199        auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
200        if auth_session:
201            SAMLSession.objects.filter(
202                session=auth_session,
203                user=self.request.user,
204                provider=self.provider,
205            ).delete()
206        return plan.to_redirect(self.request, self.flow)
207
208    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
209        """GET and POST use the same handler, but we can't
210        override .dispatch easily because PolicyAccessView's dispatch"""
211        return self.get(request, application_slug)
212
213
214class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
215    """SAML Handler for SP initiated SLO/Redirect bindings, which are sent via GET"""
216
217    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
218        """Override dispatch to handle logout responses before authentication check"""
219        # Check if this is a LogoutResponse before doing any authentication checks
220        # If we receive a logoutResponse, this means we are using native redirect
221        # IDP SLO, so we want to redirect to our next provider
222        if REQUEST_KEY_SAML_RESPONSE in request.GET:
223            relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
224            redirect_url = _get_redirect_url(request, relay_state)
225            if redirect_url:
226                return redirect(redirect_url)
227            return redirect("authentik_core:root-redirect")
228
229        # For SAML logout requests, use the parent dispatch with auth checks
230        return super().dispatch(request, *args, **kwargs)
231
232    def check_saml_request(self) -> HttpRequest | None:
233        # Logout responses are now handled in dispatch()
234        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
235            LOGGER.info("check_saml_request: SAML payload missing")
236            return bad_request_message(self.request, "The SAML request payload is missing.")
237
238        try:
239            logout_request = LogoutRequestParser(self.provider).parse_detached(
240                self.request.GET[REQUEST_KEY_SAML_REQUEST],
241                relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
242            )
243            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
244        except CannotHandleAssertion as exc:
245            Event.new(
246                EventAction.CONFIGURATION_ERROR,
247                provider=self.provider,
248                message=str(exc),
249            ).save()
250            LOGGER.info(str(exc))
251            return bad_request_message(self.request, str(exc))
252        return None
253
254
255@method_decorator(xframe_options_sameorigin, name="dispatch")
256@method_decorator(csrf_exempt, name="dispatch")
257class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
258    """SAML Handler for SP-initiated SLO with POST binding"""
259
260    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
261        """Override dispatch to handle logout requests and responses"""
262        # Check if this is a LogoutResponse before doing any authentication checks
263        # If we receive a logoutResponse, this means we are using native redirect
264        # IDP SLO, so we want to redirect to our next provider
265        if REQUEST_KEY_SAML_RESPONSE in request.POST:
266            relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
267            redirect_url = _get_redirect_url(request, relay_state)
268            if redirect_url:
269                return redirect(redirect_url)
270            return redirect("authentik_core:root-redirect")
271
272        # For SAML logout requests, use the parent dispatch with auth checks
273        return super().dispatch(request, *args, **kwargs)
274
275    def check_saml_request(self) -> HttpRequest | None:
276        payload = self.request.POST
277        # Logout responses are now handled in dispatch()
278        if REQUEST_KEY_SAML_REQUEST not in payload:
279            LOGGER.info("check_saml_request: SAML payload missing")
280            return bad_request_message(self.request, "The SAML request payload is missing.")
281
282        try:
283            logout_request = LogoutRequestParser(self.provider).parse(
284                payload[REQUEST_KEY_SAML_REQUEST],
285                relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
286            )
287            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
288        except CannotHandleAssertion as exc:
289            LOGGER.info(str(exc))
290            return bad_request_message(self.request, str(exc))
291        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            if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
112                # Native mode - user will be redirected/posted away from authentik
113                processor = LogoutResponseProcessor(
114                    self.provider,
115                    logout_request,
116                    destination=self.provider.sls_url,
117                )
118
119                if self.provider.sls_binding == SAMLBindings.POST:
120                    logout_response = processor.encode_post()
121                    logout_data = {
122                        "post_url": self.provider.sls_url,
123                        "saml_response": logout_response,
124                        "saml_relay_state": relay_state,
125                        "provider_name": self.provider.name,
126                        "saml_binding": SAMLBindings.POST,
127                    }
128                else:
129                    logout_url = processor.get_redirect_url()
130                    logout_data = {
131                        "redirect_url": logout_url,
132                        "provider_name": self.provider.name,
133                        "saml_binding": SAMLBindings.REDIRECT,
134                    }
135
136                plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
137                plan.append_stage(in_memory_stage(NativeLogoutStageView))
138            elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
139                # Backchannel mode - server sends logout response directly to SP in background
140                # No user interaction needed
141                if self.provider.sls_binding != SAMLBindings.POST:
142                    LOGGER.warning(
143                        "Backchannel logout requires POST binding, but provider is configured "
144                        "with %s binding",
145                        self.provider.sls_binding,
146                        provider=self.provider,
147                    )
148
149                # Queue the logout response to be sent in the background
150                # This doesn't block the user's logout from completing
151                send_saml_logout_response.send(
152                    provider_pk=self.provider.pk,
153                    sls_url=self.provider.sls_url,
154                    logout_request_id=logout_request.id if logout_request else None,
155                    relay_state=relay_state,
156                )
157
158                LOGGER.debug(
159                    "Queued backchannel logout response",
160                    provider=self.provider,
161                    sls_url=self.provider.sls_url,
162                )
163
164                # Just end the session - no user interaction needed
165                plan.append_stage(in_memory_stage(SessionEndStage))
166            else:
167                # Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
168                processor = LogoutResponseProcessor(
169                    self.provider,
170                    logout_request,
171                    destination=self.provider.sls_url,
172                )
173
174                logout_response = processor.build_response()
175
176                if self.provider.sls_binding == SAMLBindings.POST:
177                    logout_data = {
178                        "url": self.provider.sls_url,
179                        "saml_response": nice64(logout_response),
180                        "saml_relay_state": relay_state,
181                        "provider_name": self.provider.name,
182                        "binding": SAMLBindings.POST,
183                    }
184                else:
185                    logout_url = processor.get_redirect_url()
186                    logout_data = {
187                        "url": logout_url,
188                        "provider_name": self.provider.name,
189                        "binding": SAMLBindings.REDIRECT,
190                    }
191
192                plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
193                plan.append_stage(in_memory_stage(IframeLogoutStageView))
194                plan.append_stage(in_memory_stage(SessionEndStage))
195        else:
196            # No SLS URL configured, just end session
197            plan.append_stage(in_memory_stage(SessionEndStage))
198
199        # Remove samlsession from database
200        auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
201        if auth_session:
202            SAMLSession.objects.filter(
203                session=auth_session,
204                user=self.request.user,
205                provider=self.provider,
206            ).delete()
207        return plan.to_redirect(self.request, self.flow)
208
209    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
210        """GET and POST use the same handler, but we can't
211        override .dispatch easily because PolicyAccessView's dispatch"""
212        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            if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
112                # Native mode - user will be redirected/posted away from authentik
113                processor = LogoutResponseProcessor(
114                    self.provider,
115                    logout_request,
116                    destination=self.provider.sls_url,
117                )
118
119                if self.provider.sls_binding == SAMLBindings.POST:
120                    logout_response = processor.encode_post()
121                    logout_data = {
122                        "post_url": self.provider.sls_url,
123                        "saml_response": logout_response,
124                        "saml_relay_state": relay_state,
125                        "provider_name": self.provider.name,
126                        "saml_binding": SAMLBindings.POST,
127                    }
128                else:
129                    logout_url = processor.get_redirect_url()
130                    logout_data = {
131                        "redirect_url": logout_url,
132                        "provider_name": self.provider.name,
133                        "saml_binding": SAMLBindings.REDIRECT,
134                    }
135
136                plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
137                plan.append_stage(in_memory_stage(NativeLogoutStageView))
138            elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
139                # Backchannel mode - server sends logout response directly to SP in background
140                # No user interaction needed
141                if self.provider.sls_binding != SAMLBindings.POST:
142                    LOGGER.warning(
143                        "Backchannel logout requires POST binding, but provider is configured "
144                        "with %s binding",
145                        self.provider.sls_binding,
146                        provider=self.provider,
147                    )
148
149                # Queue the logout response to be sent in the background
150                # This doesn't block the user's logout from completing
151                send_saml_logout_response.send(
152                    provider_pk=self.provider.pk,
153                    sls_url=self.provider.sls_url,
154                    logout_request_id=logout_request.id if logout_request else None,
155                    relay_state=relay_state,
156                )
157
158                LOGGER.debug(
159                    "Queued backchannel logout response",
160                    provider=self.provider,
161                    sls_url=self.provider.sls_url,
162                )
163
164                # Just end the session - no user interaction needed
165                plan.append_stage(in_memory_stage(SessionEndStage))
166            else:
167                # Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
168                processor = LogoutResponseProcessor(
169                    self.provider,
170                    logout_request,
171                    destination=self.provider.sls_url,
172                )
173
174                logout_response = processor.build_response()
175
176                if self.provider.sls_binding == SAMLBindings.POST:
177                    logout_data = {
178                        "url": self.provider.sls_url,
179                        "saml_response": nice64(logout_response),
180                        "saml_relay_state": relay_state,
181                        "provider_name": self.provider.name,
182                        "binding": SAMLBindings.POST,
183                    }
184                else:
185                    logout_url = processor.get_redirect_url()
186                    logout_data = {
187                        "url": logout_url,
188                        "provider_name": self.provider.name,
189                        "binding": SAMLBindings.REDIRECT,
190                    }
191
192                plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
193                plan.append_stage(in_memory_stage(IframeLogoutStageView))
194                plan.append_stage(in_memory_stage(SessionEndStage))
195        else:
196            # No SLS URL configured, just end session
197            plan.append_stage(in_memory_stage(SessionEndStage))
198
199        # Remove samlsession from database
200        auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
201        if auth_session:
202            SAMLSession.objects.filter(
203                session=auth_session,
204                user=self.request.user,
205                provider=self.provider,
206            ).delete()
207        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:
209    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
210        """GET and POST use the same handler, but we can't
211        override .dispatch easily because PolicyAccessView's dispatch"""
212        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):
215class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
216    """SAML Handler for SP initiated SLO/Redirect bindings, which are sent via GET"""
217
218    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
219        """Override dispatch to handle logout responses before authentication check"""
220        # Check if this is a LogoutResponse before doing any authentication checks
221        # If we receive a logoutResponse, this means we are using native redirect
222        # IDP SLO, so we want to redirect to our next provider
223        if REQUEST_KEY_SAML_RESPONSE in request.GET:
224            relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
225            redirect_url = _get_redirect_url(request, relay_state)
226            if redirect_url:
227                return redirect(redirect_url)
228            return redirect("authentik_core:root-redirect")
229
230        # For SAML logout requests, use the parent dispatch with auth checks
231        return super().dispatch(request, *args, **kwargs)
232
233    def check_saml_request(self) -> HttpRequest | None:
234        # Logout responses are now handled in dispatch()
235        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
236            LOGGER.info("check_saml_request: SAML payload missing")
237            return bad_request_message(self.request, "The SAML request payload is missing.")
238
239        try:
240            logout_request = LogoutRequestParser(self.provider).parse_detached(
241                self.request.GET[REQUEST_KEY_SAML_REQUEST],
242                relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
243            )
244            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
245        except CannotHandleAssertion as exc:
246            Event.new(
247                EventAction.CONFIGURATION_ERROR,
248                provider=self.provider,
249                message=str(exc),
250            ).save()
251            LOGGER.info(str(exc))
252            return bad_request_message(self.request, str(exc))
253        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:
218    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
219        """Override dispatch to handle logout responses before authentication check"""
220        # Check if this is a LogoutResponse before doing any authentication checks
221        # If we receive a logoutResponse, this means we are using native redirect
222        # IDP SLO, so we want to redirect to our next provider
223        if REQUEST_KEY_SAML_RESPONSE in request.GET:
224            relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
225            redirect_url = _get_redirect_url(request, relay_state)
226            if redirect_url:
227                return redirect(redirect_url)
228            return redirect("authentik_core:root-redirect")
229
230        # For SAML logout requests, use the parent dispatch with auth checks
231        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:
233    def check_saml_request(self) -> HttpRequest | None:
234        # Logout responses are now handled in dispatch()
235        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
236            LOGGER.info("check_saml_request: SAML payload missing")
237            return bad_request_message(self.request, "The SAML request payload is missing.")
238
239        try:
240            logout_request = LogoutRequestParser(self.provider).parse_detached(
241                self.request.GET[REQUEST_KEY_SAML_REQUEST],
242                relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
243            )
244            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
245        except CannotHandleAssertion as exc:
246            Event.new(
247                EventAction.CONFIGURATION_ERROR,
248                provider=self.provider,
249                message=str(exc),
250            ).save()
251            LOGGER.info(str(exc))
252            return bad_request_message(self.request, str(exc))
253        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):
256@method_decorator(xframe_options_sameorigin, name="dispatch")
257@method_decorator(csrf_exempt, name="dispatch")
258class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
259    """SAML Handler for SP-initiated SLO with POST binding"""
260
261    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
262        """Override dispatch to handle logout requests and responses"""
263        # Check if this is a LogoutResponse before doing any authentication checks
264        # If we receive a logoutResponse, this means we are using native redirect
265        # IDP SLO, so we want to redirect to our next provider
266        if REQUEST_KEY_SAML_RESPONSE in request.POST:
267            relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
268            redirect_url = _get_redirect_url(request, relay_state)
269            if redirect_url:
270                return redirect(redirect_url)
271            return redirect("authentik_core:root-redirect")
272
273        # For SAML logout requests, use the parent dispatch with auth checks
274        return super().dispatch(request, *args, **kwargs)
275
276    def check_saml_request(self) -> HttpRequest | None:
277        payload = self.request.POST
278        # Logout responses are now handled in dispatch()
279        if REQUEST_KEY_SAML_REQUEST not in payload:
280            LOGGER.info("check_saml_request: SAML payload missing")
281            return bad_request_message(self.request, "The SAML request payload is missing.")
282
283        try:
284            logout_request = LogoutRequestParser(self.provider).parse(
285                payload[REQUEST_KEY_SAML_REQUEST],
286                relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
287            )
288            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
289        except CannotHandleAssertion as exc:
290            LOGGER.info(str(exc))
291            return bad_request_message(self.request, str(exc))
292        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:
261    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
262        """Override dispatch to handle logout requests and responses"""
263        # Check if this is a LogoutResponse before doing any authentication checks
264        # If we receive a logoutResponse, this means we are using native redirect
265        # IDP SLO, so we want to redirect to our next provider
266        if REQUEST_KEY_SAML_RESPONSE in request.POST:
267            relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
268            redirect_url = _get_redirect_url(request, relay_state)
269            if redirect_url:
270                return redirect(redirect_url)
271            return redirect("authentik_core:root-redirect")
272
273        # For SAML logout requests, use the parent dispatch with auth checks
274        return super().dispatch(request, *args, **kwargs)

Override dispatch to handle logout requests and responses

def check_saml_request(self) -> django.http.request.HttpRequest | None:
276    def check_saml_request(self) -> HttpRequest | None:
277        payload = self.request.POST
278        # Logout responses are now handled in dispatch()
279        if REQUEST_KEY_SAML_REQUEST not in payload:
280            LOGGER.info("check_saml_request: SAML payload missing")
281            return bad_request_message(self.request, "The SAML request payload is missing.")
282
283        try:
284            logout_request = LogoutRequestParser(self.provider).parse(
285                payload[REQUEST_KEY_SAML_REQUEST],
286                relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
287            )
288            self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request
289        except CannotHandleAssertion as exc:
290            LOGGER.info(str(exc))
291            return bad_request_message(self.request, str(exc))
292        return None

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