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
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
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 # 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
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
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
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
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
Inherited Members
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
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
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