authentik.providers.saml.views.unified
Unified SAML endpoint - handles SSO and SLO based on message type
1"""Unified SAML endpoint - handles SSO and SLO based on message type""" 2 3from base64 import b64decode 4 5from defusedxml.lxml import fromstring 6from django.http import HttpRequest, HttpResponse 7from django.utils.decorators import method_decorator 8from django.views import View 9from django.views.decorators.clickjacking import xframe_options_sameorigin 10from django.views.decorators.csrf import csrf_exempt 11from structlog.stdlib import get_logger 12 13from authentik.common.saml.constants import NS_MAP 14from authentik.flows.views.executor import SESSION_KEY_POST 15from authentik.lib.views import bad_request_message 16from authentik.providers.saml.utils.encoding import decode_base64_and_inflate 17from authentik.providers.saml.views.flows import ( 18 REQUEST_KEY_SAML_REQUEST, 19 REQUEST_KEY_SAML_RESPONSE, 20) 21from authentik.providers.saml.views.sp_slo import ( 22 SPInitiatedSLOBindingPOSTView, 23 SPInitiatedSLOBindingRedirectView, 24) 25from authentik.providers.saml.views.sso import ( 26 SAMLSSOBindingPOSTView, 27 SAMLSSOBindingRedirectView, 28) 29 30LOGGER = get_logger() 31 32# SAML message type constants 33SAML_MESSAGE_TYPE_AUTHN_REQUEST = "AuthnRequest" 34SAML_MESSAGE_TYPE_LOGOUT_REQUEST = "LogoutRequest" 35 36 37def detect_saml_message_type(saml_request: str, is_post_binding: bool) -> str | None: 38 """Parse SAML request to determine if AuthnRequest or LogoutRequest.""" 39 try: 40 if is_post_binding: 41 decoded_xml = b64decode(saml_request.encode()) 42 else: 43 decoded_xml = decode_base64_and_inflate(saml_request) 44 45 if isinstance(decoded_xml, str): 46 decoded_xml = decoded_xml.encode() 47 48 root = fromstring(decoded_xml) 49 if len(root.xpath("//samlp:AuthnRequest", namespaces=NS_MAP)): 50 return SAML_MESSAGE_TYPE_AUTHN_REQUEST 51 if len(root.xpath("//samlp:LogoutRequest", namespaces=NS_MAP)): 52 return SAML_MESSAGE_TYPE_LOGOUT_REQUEST 53 return None 54 except Exception: # noqa: BLE001 55 return None 56 57 58@method_decorator(xframe_options_sameorigin, name="dispatch") 59@method_decorator(csrf_exempt, name="dispatch") 60class SAMLUnifiedView(View): 61 """Unified SAML endpoint - handles SSO and SLO based on message type. 62 63 The operation type is determined by parsing 64 the incoming SAML message: 65 - AuthnRequest -> SSO flow (delegates to SAMLSSOBindingRedirectView/POSTView) 66 - LogoutRequest -> SLO flow (delegates to SPInitiatedSLOBindingRedirectView/POSTView) 67 - LogoutResponse -> SLO completion (delegates to SPInitiatedSLOBindingRedirectView/POSTView) 68 """ 69 70 def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse: 71 """Route the request based on SAML message type.""" 72 # ak user was not logged in, redirected to login, and is back w POST payload in session 73 if SESSION_KEY_POST in request.session: 74 return self._delegate_to_sso(request, application_slug, is_post_binding=True) 75 76 # Determine binding from HTTP method 77 is_post_binding = request.method == "POST" 78 data = request.POST if is_post_binding else request.GET 79 80 # LogoutResponse - delegate to SLO view (handles it in dispatch) 81 if REQUEST_KEY_SAML_RESPONSE in data: 82 return self._delegate_to_slo(request, application_slug, is_post_binding) 83 84 # Check for SAML request 85 if REQUEST_KEY_SAML_REQUEST not in data: 86 LOGGER.info("SAML payload missing") 87 return bad_request_message(request, "The SAML request payload is missing.") 88 89 # Detect message type and delegate 90 saml_request = data[REQUEST_KEY_SAML_REQUEST] 91 message_type = detect_saml_message_type(saml_request, is_post_binding) 92 93 if message_type == SAML_MESSAGE_TYPE_AUTHN_REQUEST: 94 return self._delegate_to_sso(request, application_slug, is_post_binding) 95 elif message_type == SAML_MESSAGE_TYPE_LOGOUT_REQUEST: 96 return self._delegate_to_slo(request, application_slug, is_post_binding) 97 else: 98 LOGGER.warning("Unknown SAML message type", message_type=message_type) 99 return bad_request_message( 100 request, f"Unsupported SAML message type: {message_type or 'unknown'}" 101 ) 102 103 def _delegate_to_sso( 104 self, request: HttpRequest, application_slug: str, is_post_binding: bool 105 ) -> HttpResponse: 106 """Delegate to the appropriate SSO view.""" 107 if is_post_binding: 108 view = SAMLSSOBindingPOSTView.as_view() 109 else: 110 view = SAMLSSOBindingRedirectView.as_view() 111 return view(request, application_slug=application_slug) 112 113 def _delegate_to_slo( 114 self, request: HttpRequest, application_slug: str, is_post_binding: bool 115 ) -> HttpResponse: 116 """Delegate to the appropriate SLO view.""" 117 if is_post_binding: 118 view = SPInitiatedSLOBindingPOSTView.as_view() 119 else: 120 view = SPInitiatedSLOBindingRedirectView.as_view() 121 return view(request, application_slug=application_slug)
LOGGER =
<BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
SAML_MESSAGE_TYPE_AUTHN_REQUEST =
'AuthnRequest'
SAML_MESSAGE_TYPE_LOGOUT_REQUEST =
'LogoutRequest'
def
detect_saml_message_type(saml_request: str, is_post_binding: bool) -> str | None:
38def detect_saml_message_type(saml_request: str, is_post_binding: bool) -> str | None: 39 """Parse SAML request to determine if AuthnRequest or LogoutRequest.""" 40 try: 41 if is_post_binding: 42 decoded_xml = b64decode(saml_request.encode()) 43 else: 44 decoded_xml = decode_base64_and_inflate(saml_request) 45 46 if isinstance(decoded_xml, str): 47 decoded_xml = decoded_xml.encode() 48 49 root = fromstring(decoded_xml) 50 if len(root.xpath("//samlp:AuthnRequest", namespaces=NS_MAP)): 51 return SAML_MESSAGE_TYPE_AUTHN_REQUEST 52 if len(root.xpath("//samlp:LogoutRequest", namespaces=NS_MAP)): 53 return SAML_MESSAGE_TYPE_LOGOUT_REQUEST 54 return None 55 except Exception: # noqa: BLE001 56 return None
Parse SAML request to determine if AuthnRequest or LogoutRequest.
@method_decorator(xframe_options_sameorigin, name='dispatch')
@method_decorator(csrf_exempt, name='dispatch')
class
SAMLUnifiedView59@method_decorator(xframe_options_sameorigin, name="dispatch") 60@method_decorator(csrf_exempt, name="dispatch") 61class SAMLUnifiedView(View): 62 """Unified SAML endpoint - handles SSO and SLO based on message type. 63 64 The operation type is determined by parsing 65 the incoming SAML message: 66 - AuthnRequest -> SSO flow (delegates to SAMLSSOBindingRedirectView/POSTView) 67 - LogoutRequest -> SLO flow (delegates to SPInitiatedSLOBindingRedirectView/POSTView) 68 - LogoutResponse -> SLO completion (delegates to SPInitiatedSLOBindingRedirectView/POSTView) 69 """ 70 71 def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse: 72 """Route the request based on SAML message type.""" 73 # ak user was not logged in, redirected to login, and is back w POST payload in session 74 if SESSION_KEY_POST in request.session: 75 return self._delegate_to_sso(request, application_slug, is_post_binding=True) 76 77 # Determine binding from HTTP method 78 is_post_binding = request.method == "POST" 79 data = request.POST if is_post_binding else request.GET 80 81 # LogoutResponse - delegate to SLO view (handles it in dispatch) 82 if REQUEST_KEY_SAML_RESPONSE in data: 83 return self._delegate_to_slo(request, application_slug, is_post_binding) 84 85 # Check for SAML request 86 if REQUEST_KEY_SAML_REQUEST not in data: 87 LOGGER.info("SAML payload missing") 88 return bad_request_message(request, "The SAML request payload is missing.") 89 90 # Detect message type and delegate 91 saml_request = data[REQUEST_KEY_SAML_REQUEST] 92 message_type = detect_saml_message_type(saml_request, is_post_binding) 93 94 if message_type == SAML_MESSAGE_TYPE_AUTHN_REQUEST: 95 return self._delegate_to_sso(request, application_slug, is_post_binding) 96 elif message_type == SAML_MESSAGE_TYPE_LOGOUT_REQUEST: 97 return self._delegate_to_slo(request, application_slug, is_post_binding) 98 else: 99 LOGGER.warning("Unknown SAML message type", message_type=message_type) 100 return bad_request_message( 101 request, f"Unsupported SAML message type: {message_type or 'unknown'}" 102 ) 103 104 def _delegate_to_sso( 105 self, request: HttpRequest, application_slug: str, is_post_binding: bool 106 ) -> HttpResponse: 107 """Delegate to the appropriate SSO view.""" 108 if is_post_binding: 109 view = SAMLSSOBindingPOSTView.as_view() 110 else: 111 view = SAMLSSOBindingRedirectView.as_view() 112 return view(request, application_slug=application_slug) 113 114 def _delegate_to_slo( 115 self, request: HttpRequest, application_slug: str, is_post_binding: bool 116 ) -> HttpResponse: 117 """Delegate to the appropriate SLO view.""" 118 if is_post_binding: 119 view = SPInitiatedSLOBindingPOSTView.as_view() 120 else: 121 view = SPInitiatedSLOBindingRedirectView.as_view() 122 return view(request, application_slug=application_slug)
Unified SAML endpoint - handles SSO and SLO based on message type.
The operation type is determined by parsing the incoming SAML message:
- AuthnRequest -> SSO flow (delegates to SAMLSSOBindingRedirectView/POSTView)
- LogoutRequest -> SLO flow (delegates to SPInitiatedSLOBindingRedirectView/POSTView)
- LogoutResponse -> SLO completion (delegates to SPInitiatedSLOBindingRedirectView/POSTView)
def
dispatch( self, request: django.http.request.HttpRequest, application_slug: str) -> django.http.response.HttpResponse:
71 def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse: 72 """Route the request based on SAML message type.""" 73 # ak user was not logged in, redirected to login, and is back w POST payload in session 74 if SESSION_KEY_POST in request.session: 75 return self._delegate_to_sso(request, application_slug, is_post_binding=True) 76 77 # Determine binding from HTTP method 78 is_post_binding = request.method == "POST" 79 data = request.POST if is_post_binding else request.GET 80 81 # LogoutResponse - delegate to SLO view (handles it in dispatch) 82 if REQUEST_KEY_SAML_RESPONSE in data: 83 return self._delegate_to_slo(request, application_slug, is_post_binding) 84 85 # Check for SAML request 86 if REQUEST_KEY_SAML_REQUEST not in data: 87 LOGGER.info("SAML payload missing") 88 return bad_request_message(request, "The SAML request payload is missing.") 89 90 # Detect message type and delegate 91 saml_request = data[REQUEST_KEY_SAML_REQUEST] 92 message_type = detect_saml_message_type(saml_request, is_post_binding) 93 94 if message_type == SAML_MESSAGE_TYPE_AUTHN_REQUEST: 95 return self._delegate_to_sso(request, application_slug, is_post_binding) 96 elif message_type == SAML_MESSAGE_TYPE_LOGOUT_REQUEST: 97 return self._delegate_to_slo(request, application_slug, is_post_binding) 98 else: 99 LOGGER.warning("Unknown SAML message type", message_type=message_type) 100 return bad_request_message( 101 request, f"Unsupported SAML message type: {message_type or 'unknown'}" 102 )
Route the request based on SAML message type.