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                        "issuer": processor.issuer,
 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.name,
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()
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                        "issuer": processor.issuer,
 86                        "expires": processor.session_not_on_or_after_datetime,
 87                        "expiring": True,
 88                    },
 89                )
 90        except SAMLException as exc:
 91            Event.new(
 92                EventAction.CONFIGURATION_ERROR,
 93                message=f"Failed to process SAML assertion: {str(exc)}",
 94                provider=provider,
 95            ).from_http(self.request)
 96            return self.executor.stage_invalid()
 97
 98        # Log Application Authorization
 99        Event.new(
100            EventAction.AUTHORIZE_APPLICATION,
101            authorized_application=application,
102            flow=self.executor.plan.flow_pk,
103        ).from_http(self.request)
104
105        if provider.sp_binding == SAMLBindings.POST:
106            form_attrs = delete_none_values(
107                {
108                    REQUEST_KEY_SAML_RESPONSE: nice64(response),
109                    REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
110                }
111            )
112            return super().get(
113                self.request,
114                **{
115                    "component": "ak-stage-autosubmit",
116                    "title": self.executor.plan.context.get(
117                        PLAN_CONTEXT_TITLE,
118                        _("Redirecting to {app}...".format_map({"app": application.name})),
119                    ),
120                    "url": provider.acs_url,
121                    "attrs": form_attrs,
122                },
123            )
124        if provider.sp_binding == SAMLBindings.REDIRECT:
125            Event.log_deprecation(
126                DEPRECATION_SP_BINDING_REDIRECT,
127                (
128                    "Redirect binding for Service Provider binding is deprecated "
129                    "and will be removed in a future version. Use Post binding instead."
130                ),
131                cause=provider.name,
132            )
133            url_args = {
134                REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
135            }
136            if auth_n_request.relay_state:
137                url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
138            querystring = urlencode(url_args)
139            return redirect(f"{provider.acs_url}?{querystring}")
140        return bad_request_message(request, "Invalid sp_binding specified")
141
142    def get_challenge(self, *args, **kwargs) -> Challenge:
143        return AutosubmitChallenge(data=kwargs)
144
145    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
146        # We'll never get here since the challenge redirects to the SP
147        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                        "issuer": processor.issuer,
 86                        "expires": processor.session_not_on_or_after_datetime,
 87                        "expiring": True,
 88                    },
 89                )
 90        except SAMLException as exc:
 91            Event.new(
 92                EventAction.CONFIGURATION_ERROR,
 93                message=f"Failed to process SAML assertion: {str(exc)}",
 94                provider=provider,
 95            ).from_http(self.request)
 96            return self.executor.stage_invalid()
 97
 98        # Log Application Authorization
 99        Event.new(
100            EventAction.AUTHORIZE_APPLICATION,
101            authorized_application=application,
102            flow=self.executor.plan.flow_pk,
103        ).from_http(self.request)
104
105        if provider.sp_binding == SAMLBindings.POST:
106            form_attrs = delete_none_values(
107                {
108                    REQUEST_KEY_SAML_RESPONSE: nice64(response),
109                    REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
110                }
111            )
112            return super().get(
113                self.request,
114                **{
115                    "component": "ak-stage-autosubmit",
116                    "title": self.executor.plan.context.get(
117                        PLAN_CONTEXT_TITLE,
118                        _("Redirecting to {app}...".format_map({"app": application.name})),
119                    ),
120                    "url": provider.acs_url,
121                    "attrs": form_attrs,
122                },
123            )
124        if provider.sp_binding == SAMLBindings.REDIRECT:
125            Event.log_deprecation(
126                DEPRECATION_SP_BINDING_REDIRECT,
127                (
128                    "Redirect binding for Service Provider binding is deprecated "
129                    "and will be removed in a future version. Use Post binding instead."
130                ),
131                cause=provider.name,
132            )
133            url_args = {
134                REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
135            }
136            if auth_n_request.relay_state:
137                url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
138            querystring = urlencode(url_args)
139            return redirect(f"{provider.acs_url}?{querystring}")
140        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:
142    def get_challenge(self, *args, **kwargs) -> Challenge:
143        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:
145    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
146        # We'll never get here since the challenge redirects to the SP
147        return HttpResponseBadRequest()

Callback when the challenge has the correct format