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'
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).
response_class =
<class 'authentik.flows.challenge.AutoSubmitChallengeResponse'>
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
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