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 SAMLUnifiedView(django.views.generic.base.View):
 59@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.