authentik.providers.saml.views.flows

authentik SAML IDP Views

  1"""authentik SAML IDP Views"""
  2
  3from django.core.validators import URLValidator
  4from django.http import HttpRequest, HttpResponse
  5from django.http.response import HttpResponseBadRequest
  6from django.shortcuts import get_object_or_404, redirect
  7from django.utils.http import urlencode
  8from django.utils.translation import gettext as _
  9from structlog.stdlib import get_logger
 10
 11from authentik.core.models import Application, AuthenticatedSession
 12from authentik.events.models import Event, EventAction
 13from authentik.flows.challenge import (
 14    PLAN_CONTEXT_TITLE,
 15    AutosubmitChallenge,
 16    AutoSubmitChallengeResponse,
 17    Challenge,
 18    ChallengeResponse,
 19)
 20from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
 21from authentik.flows.stage import ChallengeStageView
 22from authentik.lib.views import bad_request_message
 23from authentik.policies.utils import delete_none_values
 24from authentik.providers.saml.models import SAMLBindings, SAMLProvider, SAMLSession
 25from authentik.providers.saml.processors.assertion import AssertionProcessor
 26from authentik.providers.saml.processors.authn_request_parser import AuthNRequest
 27from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
 28from authentik.sources.saml.exceptions import SAMLException
 29
 30LOGGER = get_logger()
 31URL_VALIDATOR = URLValidator(schemes=("http", "https"))
 32REQUEST_KEY_SAML_REQUEST = "SAMLRequest"
 33REQUEST_KEY_SAML_SIGNATURE = "Signature"
 34REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
 35REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
 36REQUEST_KEY_RELAY_STATE = "RelayState"
 37
 38DEPRECATION_SP_BINDING_REDIRECT = "authentik.providers.saml.sp_binding_redirect"
 39
 40PLAN_CONTEXT_SAML_AUTH_N_REQUEST = "authentik/providers/saml/authn_request"
 41PLAN_CONTEXT_SAML_LOGOUT_REQUEST = "authentik/providers/saml/logout_request"
 42PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS = "goauthentik.io/providers/saml/native_sessions"
 43PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS = "goauthentik.io/providers/saml/iframe_sessions"
 44PLAN_CONTEXT_SAML_RELAY_STATE = "goauthentik.io/providers/saml/relay_state"
 45
 46
 47# This View doesn't have a URL on purpose, as its called by the FlowExecutor
 48class SAMLFlowFinalView(ChallengeStageView):
 49    """View used by FlowExecutor after all stages have passed. Logs the authorization,
 50    and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element
 51    (if POST is configured)."""
 52
 53    response_class = AutoSubmitChallengeResponse
 54
 55    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 56        application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
 57        provider: SAMLProvider = get_object_or_404(SAMLProvider, pk=application.provider_id)
 58        if PLAN_CONTEXT_SAML_AUTH_N_REQUEST not in self.executor.plan.context:
 59            self.logger.warning("No AuthNRequest in context")
 60            return self.executor.stage_invalid()
 61
 62        auth_n_request: AuthNRequest = self.executor.plan.context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST]
 63        try:
 64            processor = AssertionProcessor(provider, request, auth_n_request)
 65            response = processor.build_response()
 66
 67            # Create SAMLSession to track this login
 68            auth_session = AuthenticatedSession.from_request(request, request.user)
 69            if auth_session:
 70                # Since samlsessions should only exist uniquely for an active session and a provider
 71                # any existing combination is likely an old, dead session
 72                SAMLSession.objects.filter(
 73                    session_index=processor.session_index, provider=provider
 74                ).delete()
 75
 76                SAMLSession.objects.update_or_create(
 77                    session_index=processor.session_index,
 78                    provider=provider,
 79                    defaults={
 80                        "user": request.user,
 81                        "session": auth_session,
 82                        "name_id": processor.name_id,
 83                        "name_id_format": processor.name_id_format,
 84                        "expires": processor.session_not_on_or_after_datetime,
 85                        "expiring": True,
 86                    },
 87                )
 88        except SAMLException as exc:
 89            Event.new(
 90                EventAction.CONFIGURATION_ERROR,
 91                message=f"Failed to process SAML assertion: {str(exc)}",
 92                provider=provider,
 93            ).from_http(self.request)
 94            return self.executor.stage_invalid()
 95
 96        # Log Application Authorization
 97        Event.new(
 98            EventAction.AUTHORIZE_APPLICATION,
 99            authorized_application=application,
100            flow=self.executor.plan.flow_pk,
101        ).from_http(self.request)
102
103        if provider.sp_binding == SAMLBindings.POST:
104            form_attrs = delete_none_values(
105                {
106                    REQUEST_KEY_SAML_RESPONSE: nice64(response),
107                    REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
108                }
109            )
110            return super().get(
111                self.request,
112                **{
113                    "component": "ak-stage-autosubmit",
114                    "title": self.executor.plan.context.get(
115                        PLAN_CONTEXT_TITLE,
116                        _("Redirecting to {app}...".format_map({"app": application.name})),
117                    ),
118                    "url": provider.acs_url,
119                    "attrs": form_attrs,
120                },
121            )
122        if provider.sp_binding == SAMLBindings.REDIRECT:
123            Event.log_deprecation(
124                DEPRECATION_SP_BINDING_REDIRECT,
125                (
126                    "Redirect binding for Service Provider binding is deprecated "
127                    "and will be removed in a future version. Use Post binding instead."
128                ),
129                cause=provider,
130            )
131            url_args = {
132                REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
133            }
134            if auth_n_request.relay_state:
135                url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
136            querystring = urlencode(url_args)
137            return redirect(f"{provider.acs_url}?{querystring}")
138        return bad_request_message(request, "Invalid sp_binding specified")
139
140    def get_challenge(self, *args, **kwargs) -> Challenge:
141        return AutosubmitChallenge(data=kwargs)
142
143    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
144        # We'll never get here since the challenge redirects to the SP
145        return HttpResponseBadRequest()
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
URL_VALIDATOR = <django.core.validators.URLValidator object>
REQUEST_KEY_SAML_REQUEST = 'SAMLRequest'
REQUEST_KEY_SAML_SIGNATURE = 'Signature'
REQUEST_KEY_SAML_SIG_ALG = 'SigAlg'
REQUEST_KEY_SAML_RESPONSE = 'SAMLResponse'
REQUEST_KEY_RELAY_STATE = 'RelayState'
DEPRECATION_SP_BINDING_REDIRECT = 'authentik.providers.saml.sp_binding_redirect'
PLAN_CONTEXT_SAML_AUTH_N_REQUEST = 'authentik/providers/saml/authn_request'
PLAN_CONTEXT_SAML_LOGOUT_REQUEST = 'authentik/providers/saml/logout_request'
PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS = 'goauthentik.io/providers/saml/native_sessions'
PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS = 'goauthentik.io/providers/saml/iframe_sessions'
PLAN_CONTEXT_SAML_RELAY_STATE = 'goauthentik.io/providers/saml/relay_state'
class SAMLFlowFinalView(authentik.flows.stage.ChallengeStageView):
 49class SAMLFlowFinalView(ChallengeStageView):
 50    """View used by FlowExecutor after all stages have passed. Logs the authorization,
 51    and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element
 52    (if POST is configured)."""
 53
 54    response_class = AutoSubmitChallengeResponse
 55
 56    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 57        application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
 58        provider: SAMLProvider = get_object_or_404(SAMLProvider, pk=application.provider_id)
 59        if PLAN_CONTEXT_SAML_AUTH_N_REQUEST not in self.executor.plan.context:
 60            self.logger.warning("No AuthNRequest in context")
 61            return self.executor.stage_invalid()
 62
 63        auth_n_request: AuthNRequest = self.executor.plan.context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST]
 64        try:
 65            processor = AssertionProcessor(provider, request, auth_n_request)
 66            response = processor.build_response()
 67
 68            # Create SAMLSession to track this login
 69            auth_session = AuthenticatedSession.from_request(request, request.user)
 70            if auth_session:
 71                # Since samlsessions should only exist uniquely for an active session and a provider
 72                # any existing combination is likely an old, dead session
 73                SAMLSession.objects.filter(
 74                    session_index=processor.session_index, provider=provider
 75                ).delete()
 76
 77                SAMLSession.objects.update_or_create(
 78                    session_index=processor.session_index,
 79                    provider=provider,
 80                    defaults={
 81                        "user": request.user,
 82                        "session": auth_session,
 83                        "name_id": processor.name_id,
 84                        "name_id_format": processor.name_id_format,
 85                        "expires": processor.session_not_on_or_after_datetime,
 86                        "expiring": True,
 87                    },
 88                )
 89        except SAMLException as exc:
 90            Event.new(
 91                EventAction.CONFIGURATION_ERROR,
 92                message=f"Failed to process SAML assertion: {str(exc)}",
 93                provider=provider,
 94            ).from_http(self.request)
 95            return self.executor.stage_invalid()
 96
 97        # Log Application Authorization
 98        Event.new(
 99            EventAction.AUTHORIZE_APPLICATION,
100            authorized_application=application,
101            flow=self.executor.plan.flow_pk,
102        ).from_http(self.request)
103
104        if provider.sp_binding == SAMLBindings.POST:
105            form_attrs = delete_none_values(
106                {
107                    REQUEST_KEY_SAML_RESPONSE: nice64(response),
108                    REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
109                }
110            )
111            return super().get(
112                self.request,
113                **{
114                    "component": "ak-stage-autosubmit",
115                    "title": self.executor.plan.context.get(
116                        PLAN_CONTEXT_TITLE,
117                        _("Redirecting to {app}...".format_map({"app": application.name})),
118                    ),
119                    "url": provider.acs_url,
120                    "attrs": form_attrs,
121                },
122            )
123        if provider.sp_binding == SAMLBindings.REDIRECT:
124            Event.log_deprecation(
125                DEPRECATION_SP_BINDING_REDIRECT,
126                (
127                    "Redirect binding for Service Provider binding is deprecated "
128                    "and will be removed in a future version. Use Post binding instead."
129                ),
130                cause=provider,
131            )
132            url_args = {
133                REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
134            }
135            if auth_n_request.relay_state:
136                url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
137            querystring = urlencode(url_args)
138            return redirect(f"{provider.acs_url}?{querystring}")
139        return bad_request_message(request, "Invalid sp_binding specified")
140
141    def get_challenge(self, *args, **kwargs) -> Challenge:
142        return AutosubmitChallenge(data=kwargs)
143
144    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
145        # We'll never get here since the challenge redirects to the SP
146        return HttpResponseBadRequest()

View used by FlowExecutor after all stages have passed. Logs the authorization, and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element (if POST is configured).

def get( self, request: django.http.request.HttpRequest, *args, **kwargs) -> django.http.response.HttpResponse:
 56    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 57        application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
 58        provider: SAMLProvider = get_object_or_404(SAMLProvider, pk=application.provider_id)
 59        if PLAN_CONTEXT_SAML_AUTH_N_REQUEST not in self.executor.plan.context:
 60            self.logger.warning("No AuthNRequest in context")
 61            return self.executor.stage_invalid()
 62
 63        auth_n_request: AuthNRequest = self.executor.plan.context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST]
 64        try:
 65            processor = AssertionProcessor(provider, request, auth_n_request)
 66            response = processor.build_response()
 67
 68            # Create SAMLSession to track this login
 69            auth_session = AuthenticatedSession.from_request(request, request.user)
 70            if auth_session:
 71                # Since samlsessions should only exist uniquely for an active session and a provider
 72                # any existing combination is likely an old, dead session
 73                SAMLSession.objects.filter(
 74                    session_index=processor.session_index, provider=provider
 75                ).delete()
 76
 77                SAMLSession.objects.update_or_create(
 78                    session_index=processor.session_index,
 79                    provider=provider,
 80                    defaults={
 81                        "user": request.user,
 82                        "session": auth_session,
 83                        "name_id": processor.name_id,
 84                        "name_id_format": processor.name_id_format,
 85                        "expires": processor.session_not_on_or_after_datetime,
 86                        "expiring": True,
 87                    },
 88                )
 89        except SAMLException as exc:
 90            Event.new(
 91                EventAction.CONFIGURATION_ERROR,
 92                message=f"Failed to process SAML assertion: {str(exc)}",
 93                provider=provider,
 94            ).from_http(self.request)
 95            return self.executor.stage_invalid()
 96
 97        # Log Application Authorization
 98        Event.new(
 99            EventAction.AUTHORIZE_APPLICATION,
100            authorized_application=application,
101            flow=self.executor.plan.flow_pk,
102        ).from_http(self.request)
103
104        if provider.sp_binding == SAMLBindings.POST:
105            form_attrs = delete_none_values(
106                {
107                    REQUEST_KEY_SAML_RESPONSE: nice64(response),
108                    REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
109                }
110            )
111            return super().get(
112                self.request,
113                **{
114                    "component": "ak-stage-autosubmit",
115                    "title": self.executor.plan.context.get(
116                        PLAN_CONTEXT_TITLE,
117                        _("Redirecting to {app}...".format_map({"app": application.name})),
118                    ),
119                    "url": provider.acs_url,
120                    "attrs": form_attrs,
121                },
122            )
123        if provider.sp_binding == SAMLBindings.REDIRECT:
124            Event.log_deprecation(
125                DEPRECATION_SP_BINDING_REDIRECT,
126                (
127                    "Redirect binding for Service Provider binding is deprecated "
128                    "and will be removed in a future version. Use Post binding instead."
129                ),
130                cause=provider,
131            )
132            url_args = {
133                REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
134            }
135            if auth_n_request.relay_state:
136                url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
137            querystring = urlencode(url_args)
138            return redirect(f"{provider.acs_url}?{querystring}")
139        return bad_request_message(request, "Invalid sp_binding specified")

Return a challenge for the frontend to solve

def get_challenge(self, *args, **kwargs) -> authentik.flows.challenge.Challenge:
141    def get_challenge(self, *args, **kwargs) -> Challenge:
142        return AutosubmitChallenge(data=kwargs)

Return the challenge that the client should solve

def challenge_valid( self, response: authentik.flows.challenge.ChallengeResponse) -> django.http.response.HttpResponse:
144    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
145        # We'll never get here since the challenge redirects to the SP
146        return HttpResponseBadRequest()

Callback when the challenge has the correct format