authentik.providers.saml.views.sso

authentik SAML IDP Views

  1"""authentik SAML IDP Views"""
  2
  3from django.http import Http404, HttpRequest, HttpResponse
  4from django.shortcuts import get_object_or_404
  5from django.utils.decorators import method_decorator
  6from django.utils.translation import gettext as _
  7from django.views.decorators.clickjacking import xframe_options_sameorigin
  8from django.views.decorators.csrf import csrf_exempt
  9from structlog.stdlib import get_logger
 10
 11from authentik.core.models import Application
 12from authentik.events.models import Event, EventAction
 13from authentik.flows.exceptions import FlowNonApplicableException
 14from authentik.flows.models import in_memory_stage
 15from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
 16from authentik.flows.views.executor import SESSION_KEY_POST
 17from authentik.lib.views import bad_request_message
 18from authentik.policies.views import PolicyAccessView
 19from authentik.providers.saml.exceptions import CannotHandleAssertion
 20from authentik.providers.saml.models import SAMLBindings, SAMLProvider
 21from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
 22from authentik.providers.saml.views.flows import (
 23    PLAN_CONTEXT_SAML_AUTH_N_REQUEST,
 24    REQUEST_KEY_RELAY_STATE,
 25    REQUEST_KEY_SAML_REQUEST,
 26    REQUEST_KEY_SAML_SIG_ALG,
 27    REQUEST_KEY_SAML_SIGNATURE,
 28    SAMLFlowFinalView,
 29)
 30from authentik.stages.consent.stage import (
 31    PLAN_CONTEXT_CONSENT_HEADER,
 32    PLAN_CONTEXT_CONSENT_PERMISSIONS,
 33)
 34
 35LOGGER = get_logger()
 36
 37
 38class SAMLSSOView(PolicyAccessView):
 39    """SAML SSO Base View, which plans a flow and injects our final stage.
 40    Calls get/post handler."""
 41
 42    def __init__(self, **kwargs):
 43        super().__init__(**kwargs)
 44        self.plan_context = {}
 45
 46    def resolve_provider_application(self):
 47        self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
 48        self.provider: SAMLProvider = get_object_or_404(
 49            SAMLProvider, pk=self.application.provider_id
 50        )
 51
 52    def check_saml_request(self) -> HttpRequest | None:
 53        """Handler to verify the SAML Request. Must be implemented by a subclass"""
 54        raise NotImplementedError
 55
 56    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
 57        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
 58        # Call the method handler, which checks the SAML
 59        # Request and returns a HTTP Response on error
 60        method_response = self.check_saml_request()
 61        if method_response:
 62            return method_response
 63        # Regardless, we start the planner and return to it
 64        planner = FlowPlanner(self.provider.authorization_flow)
 65        planner.allow_empty_flows = True
 66        try:
 67            plan = planner.plan(
 68                request,
 69                {
 70                    PLAN_CONTEXT_SSO: True,
 71                    PLAN_CONTEXT_APPLICATION: self.application,
 72                    PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
 73                    % {"application": self.application.name},
 74                    PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
 75                    **self.plan_context,
 76                },
 77            )
 78        except FlowNonApplicableException:
 79            raise Http404 from None
 80        plan.append_stage(in_memory_stage(SAMLFlowFinalView))
 81        return plan.to_redirect(
 82            request,
 83            self.provider.authorization_flow,
 84            allowed_silent_types=(
 85                [SAMLFlowFinalView] if self.provider.sp_binding in [SAMLBindings.REDIRECT] else []
 86            ),
 87        )
 88
 89    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
 90        """GET and POST use the same handler, but we can't
 91        override .dispatch easily because PolicyAccessView's dispatch"""
 92        return self.get(request, application_slug)
 93
 94
 95class SAMLSSOBindingRedirectView(SAMLSSOView):
 96    """SAML Handler for SSO/Redirect bindings, which are sent via GET"""
 97
 98    def check_saml_request(self) -> HttpRequest | None:
 99        """Handle REDIRECT bindings"""
100        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
101            LOGGER.info("SAML payload missing")
102            return bad_request_message(self.request, "The SAML request payload is missing.")
103
104        try:
105            auth_n_request = AuthNRequestParser(self.provider).parse_detached(
106                self.request.GET[REQUEST_KEY_SAML_REQUEST],
107                self.request.GET.get(REQUEST_KEY_RELAY_STATE),
108                self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
109                self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
110            )
111            self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
112        except CannotHandleAssertion as exc:
113            Event.new(
114                EventAction.CONFIGURATION_ERROR,
115                provider=self.provider,
116                message=str(exc),
117            ).save()
118            LOGGER.info(str(exc))
119            return bad_request_message(self.request, str(exc))
120        return None
121
122
123@method_decorator(xframe_options_sameorigin, name="dispatch")
124@method_decorator(csrf_exempt, name="dispatch")
125class SAMLSSOBindingPOSTView(SAMLSSOView):
126    """SAML Handler for SSO/POST bindings"""
127
128    def check_saml_request(self) -> HttpRequest | None:
129        """Handle POST bindings"""
130        payload = self.request.POST
131        # Restore the post body from the session
132        # This happens when using POST bindings but the user isn't logged in
133        # (user gets redirected and POST body is 'lost')
134        if SESSION_KEY_POST in self.request.session:
135            payload = self.request.session.pop(SESSION_KEY_POST)
136        if REQUEST_KEY_SAML_REQUEST not in payload:
137            LOGGER.info("SAML payload missing")
138            return bad_request_message(self.request, "The SAML request payload is missing.")
139
140        try:
141            auth_n_request = AuthNRequestParser(self.provider).parse(
142                payload[REQUEST_KEY_SAML_REQUEST],
143                payload.get(REQUEST_KEY_RELAY_STATE),
144            )
145            self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
146        except CannotHandleAssertion as exc:
147            LOGGER.info(str(exc))
148            return bad_request_message(self.request, str(exc))
149        return None
150
151
152class SAMLSSOBindingInitView(SAMLSSOView):
153    """SAML Handler for for IdP Initiated login flows"""
154
155    def check_saml_request(self) -> HttpRequest | None:
156        """Create SAML Response from scratch"""
157        LOGGER.debug("No SAML Request, using IdP-initiated flow.")
158        auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
159        self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class SAMLSSOView(authentik.policies.views.PolicyAccessView):
39class SAMLSSOView(PolicyAccessView):
40    """SAML SSO Base View, which plans a flow and injects our final stage.
41    Calls get/post handler."""
42
43    def __init__(self, **kwargs):
44        super().__init__(**kwargs)
45        self.plan_context = {}
46
47    def resolve_provider_application(self):
48        self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
49        self.provider: SAMLProvider = get_object_or_404(
50            SAMLProvider, pk=self.application.provider_id
51        )
52
53    def check_saml_request(self) -> HttpRequest | None:
54        """Handler to verify the SAML Request. Must be implemented by a subclass"""
55        raise NotImplementedError
56
57    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
58        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
59        # Call the method handler, which checks the SAML
60        # Request and returns a HTTP Response on error
61        method_response = self.check_saml_request()
62        if method_response:
63            return method_response
64        # Regardless, we start the planner and return to it
65        planner = FlowPlanner(self.provider.authorization_flow)
66        planner.allow_empty_flows = True
67        try:
68            plan = planner.plan(
69                request,
70                {
71                    PLAN_CONTEXT_SSO: True,
72                    PLAN_CONTEXT_APPLICATION: self.application,
73                    PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
74                    % {"application": self.application.name},
75                    PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
76                    **self.plan_context,
77                },
78            )
79        except FlowNonApplicableException:
80            raise Http404 from None
81        plan.append_stage(in_memory_stage(SAMLFlowFinalView))
82        return plan.to_redirect(
83            request,
84            self.provider.authorization_flow,
85            allowed_silent_types=(
86                [SAMLFlowFinalView] if self.provider.sp_binding in [SAMLBindings.REDIRECT] else []
87            ),
88        )
89
90    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
91        """GET and POST use the same handler, but we can't
92        override .dispatch easily because PolicyAccessView's dispatch"""
93        return self.get(request, application_slug)

SAML SSO Base View, which plans a flow and injects our final stage. Calls get/post handler.

SAMLSSOView(**kwargs)
43    def __init__(self, **kwargs):
44        super().__init__(**kwargs)
45        self.plan_context = {}

Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.

plan_context
def resolve_provider_application(self):
47    def resolve_provider_application(self):
48        self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
49        self.provider: SAMLProvider = get_object_or_404(
50            SAMLProvider, pk=self.application.provider_id
51        )

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:
53    def check_saml_request(self) -> HttpRequest | None:
54        """Handler to verify the SAML Request. Must be implemented by a subclass"""
55        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:
57    def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
58        """Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
59        # Call the method handler, which checks the SAML
60        # Request and returns a HTTP Response on error
61        method_response = self.check_saml_request()
62        if method_response:
63            return method_response
64        # Regardless, we start the planner and return to it
65        planner = FlowPlanner(self.provider.authorization_flow)
66        planner.allow_empty_flows = True
67        try:
68            plan = planner.plan(
69                request,
70                {
71                    PLAN_CONTEXT_SSO: True,
72                    PLAN_CONTEXT_APPLICATION: self.application,
73                    PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
74                    % {"application": self.application.name},
75                    PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
76                    **self.plan_context,
77                },
78            )
79        except FlowNonApplicableException:
80            raise Http404 from None
81        plan.append_stage(in_memory_stage(SAMLFlowFinalView))
82        return plan.to_redirect(
83            request,
84            self.provider.authorization_flow,
85            allowed_silent_types=(
86                [SAMLFlowFinalView] if self.provider.sp_binding in [SAMLBindings.REDIRECT] else []
87            ),
88        )

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:
90    def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
91        """GET and POST use the same handler, but we can't
92        override .dispatch easily because PolicyAccessView's dispatch"""
93        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 SAMLSSOBindingRedirectView(SAMLSSOView):
 96class SAMLSSOBindingRedirectView(SAMLSSOView):
 97    """SAML Handler for SSO/Redirect bindings, which are sent via GET"""
 98
 99    def check_saml_request(self) -> HttpRequest | None:
100        """Handle REDIRECT bindings"""
101        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
102            LOGGER.info("SAML payload missing")
103            return bad_request_message(self.request, "The SAML request payload is missing.")
104
105        try:
106            auth_n_request = AuthNRequestParser(self.provider).parse_detached(
107                self.request.GET[REQUEST_KEY_SAML_REQUEST],
108                self.request.GET.get(REQUEST_KEY_RELAY_STATE),
109                self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
110                self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
111            )
112            self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
113        except CannotHandleAssertion as exc:
114            Event.new(
115                EventAction.CONFIGURATION_ERROR,
116                provider=self.provider,
117                message=str(exc),
118            ).save()
119            LOGGER.info(str(exc))
120            return bad_request_message(self.request, str(exc))
121        return None

SAML Handler for SSO/Redirect bindings, which are sent via GET

def check_saml_request(self) -> django.http.request.HttpRequest | None:
 99    def check_saml_request(self) -> HttpRequest | None:
100        """Handle REDIRECT bindings"""
101        if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
102            LOGGER.info("SAML payload missing")
103            return bad_request_message(self.request, "The SAML request payload is missing.")
104
105        try:
106            auth_n_request = AuthNRequestParser(self.provider).parse_detached(
107                self.request.GET[REQUEST_KEY_SAML_REQUEST],
108                self.request.GET.get(REQUEST_KEY_RELAY_STATE),
109                self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
110                self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
111            )
112            self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
113        except CannotHandleAssertion as exc:
114            Event.new(
115                EventAction.CONFIGURATION_ERROR,
116                provider=self.provider,
117                message=str(exc),
118            ).save()
119            LOGGER.info(str(exc))
120            return bad_request_message(self.request, str(exc))
121        return None

Handle REDIRECT bindings

@method_decorator(xframe_options_sameorigin, name='dispatch')
@method_decorator(csrf_exempt, name='dispatch')
class SAMLSSOBindingPOSTView(SAMLSSOView):
124@method_decorator(xframe_options_sameorigin, name="dispatch")
125@method_decorator(csrf_exempt, name="dispatch")
126class SAMLSSOBindingPOSTView(SAMLSSOView):
127    """SAML Handler for SSO/POST bindings"""
128
129    def check_saml_request(self) -> HttpRequest | None:
130        """Handle POST bindings"""
131        payload = self.request.POST
132        # Restore the post body from the session
133        # This happens when using POST bindings but the user isn't logged in
134        # (user gets redirected and POST body is 'lost')
135        if SESSION_KEY_POST in self.request.session:
136            payload = self.request.session.pop(SESSION_KEY_POST)
137        if REQUEST_KEY_SAML_REQUEST not in payload:
138            LOGGER.info("SAML payload missing")
139            return bad_request_message(self.request, "The SAML request payload is missing.")
140
141        try:
142            auth_n_request = AuthNRequestParser(self.provider).parse(
143                payload[REQUEST_KEY_SAML_REQUEST],
144                payload.get(REQUEST_KEY_RELAY_STATE),
145            )
146            self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
147        except CannotHandleAssertion as exc:
148            LOGGER.info(str(exc))
149            return bad_request_message(self.request, str(exc))
150        return None

SAML Handler for SSO/POST bindings

def check_saml_request(self) -> django.http.request.HttpRequest | None:
129    def check_saml_request(self) -> HttpRequest | None:
130        """Handle POST bindings"""
131        payload = self.request.POST
132        # Restore the post body from the session
133        # This happens when using POST bindings but the user isn't logged in
134        # (user gets redirected and POST body is 'lost')
135        if SESSION_KEY_POST in self.request.session:
136            payload = self.request.session.pop(SESSION_KEY_POST)
137        if REQUEST_KEY_SAML_REQUEST not in payload:
138            LOGGER.info("SAML payload missing")
139            return bad_request_message(self.request, "The SAML request payload is missing.")
140
141        try:
142            auth_n_request = AuthNRequestParser(self.provider).parse(
143                payload[REQUEST_KEY_SAML_REQUEST],
144                payload.get(REQUEST_KEY_RELAY_STATE),
145            )
146            self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request
147        except CannotHandleAssertion as exc:
148            LOGGER.info(str(exc))
149            return bad_request_message(self.request, str(exc))
150        return None

Handle POST bindings

def dispatch( self, request: django.http.request.HttpRequest, *args: Any, **kwargs: Any) -> django.http.response.HttpResponse:
63    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
64        try:
65            self.pre_permission_check()
66        except RequestValidationError as exc:
67            if exc.response:
68                return exc.response
69            return self.handle_no_permission()
70        try:
71            self.resolve_provider_application()
72        except (Application.DoesNotExist, Provider.DoesNotExist) as exc:
73            LOGGER.warning("failed to resolve application", exc=exc)
74            return self.handle_no_permission_authenticated(
75                PolicyResult(False, _("Failed to resolve application"))
76            )
77        # Check if user is unauthenticated, so we pass the application
78        # for the identification stage
79        if not request.user.is_authenticated:
80            return self.handle_no_permission()
81        # Check permissions
82        result = self.user_has_access()
83        if not result.passing:
84            return self.handle_no_permission_authenticated(result)
85        return super().dispatch(request, *args, **kwargs)
class SAMLSSOBindingInitView(SAMLSSOView):
153class SAMLSSOBindingInitView(SAMLSSOView):
154    """SAML Handler for for IdP Initiated login flows"""
155
156    def check_saml_request(self) -> HttpRequest | None:
157        """Create SAML Response from scratch"""
158        LOGGER.debug("No SAML Request, using IdP-initiated flow.")
159        auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
160        self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request

SAML Handler for for IdP Initiated login flows

def check_saml_request(self) -> django.http.request.HttpRequest | None:
156    def check_saml_request(self) -> HttpRequest | None:
157        """Create SAML Response from scratch"""
158        LOGGER.debug("No SAML Request, using IdP-initiated flow.")
159        auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
160        self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request

Create SAML Response from scratch