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
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
Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.
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
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
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
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
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
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
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
Inherited Members
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
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
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