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
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.
Constructor. Called in the URLconf; can contain helpful extra keyword arguments, and other things.
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
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
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
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
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
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
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
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
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)
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
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