diff --git a/passbook/providers/saml/processors/assertion.py b/passbook/providers/saml/processors/assertion.py index ce3af0503..2151da7da 100644 --- a/passbook/providers/saml/processors/assertion.py +++ b/passbook/providers/saml/processors/assertion.py @@ -1,4 +1,5 @@ """SAML Assertion generator""" +from hashlib import sha256 from types import GeneratorType from django.http import HttpRequest @@ -12,12 +13,16 @@ from passbook.providers.saml.models import SAMLPropertyMapping, SAMLProvider from passbook.providers.saml.processors.request_parser import AuthNRequest from passbook.providers.saml.utils import get_random_id from passbook.providers.saml.utils.time import get_time_string, timedelta_from_string +from passbook.sources.saml.exceptions import UnsupportedNameIDFormat from passbook.sources.saml.processors.constants import ( NS_MAP, NS_SAML_ASSERTION, NS_SAML_PROTOCOL, NS_SIGNATURE, SAML_NAME_ID_FORMAT_EMAIL, + SAML_NAME_ID_FORMAT_PRESISTENT, + SAML_NAME_ID_FORMAT_TRANSIENT, + SAML_NAME_ID_FORMAT_X509, ) LOGGER = get_logger() @@ -127,14 +132,35 @@ class AssertionProcessor: audience.text = self.provider.audience return conditions + def get_name_id(self) -> Element: + """Get NameID Element""" + name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID") + name_id.attrib["Format"] = self.auth_n_request.name_id_policy + if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL: + name_id.text = self.http_request.user.email + return name_id + if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_PRESISTENT: + name_id.text = self.http_request.user.username + return name_id + if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509: + # This attribute is statically set by the LDAP source + name_id.text = self.http_request.user.attributes.get( + "distinguishedName", "" + ) + return name_id + if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT: + # This attribute is statically set by the LDAP source + session_key: str = self.http_request.user.session.session_key + name_id.text = sha256(session_key.encode()).hexdigest() + return name_id + raise UnsupportedNameIDFormat( + f"Assertion contains NameID with unsupported format {name_id.attrib['Format']}." + ) + def get_assertion_subject(self) -> Element: """Generate Subject Element with NameID and SubjectConfirmation Objects""" subject = Element(f"{{{NS_SAML_ASSERTION}}}Subject") - - name_id = SubElement(subject, f"{{{NS_SAML_ASSERTION}}}NameID") - # TODO: Make this configurable - name_id.attrib["Format"] = SAML_NAME_ID_FORMAT_EMAIL - name_id.text = self.http_request.user.email + subject.append(self.get_name_id()) subject_confirmation = SubElement( subject, f"{{{NS_SAML_ASSERTION}}}SubjectConfirmation" diff --git a/passbook/providers/saml/processors/metadata.py b/passbook/providers/saml/processors/metadata.py index 071d41ec1..84a665dde 100644 --- a/passbook/providers/saml/processors/metadata.py +++ b/passbook/providers/saml/processors/metadata.py @@ -17,7 +17,6 @@ from passbook.sources.saml.processors.constants import ( SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_PRESISTENT, SAML_NAME_ID_FORMAT_TRANSIENT, - SAML_NAME_ID_FORMAT_WINDOWS, SAML_NAME_ID_FORMAT_X509, ) @@ -54,7 +53,6 @@ class MetadataProcessor: SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_PRESISTENT, SAML_NAME_ID_FORMAT_X509, - SAML_NAME_ID_FORMAT_WINDOWS, SAML_NAME_ID_FORMAT_TRANSIENT, ] for name_id_format in formats: diff --git a/passbook/providers/saml/views.py b/passbook/providers/saml/views.py index f8745a8d1..1925ecb89 100644 --- a/passbook/providers/saml/views.py +++ b/passbook/providers/saml/views.py @@ -38,11 +38,12 @@ from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE LOGGER = get_logger() URL_VALIDATOR = URLValidator(schemes=("http", "https")) -SESSION_KEY_SAML_REQUEST = "SAMLRequest" -SESSION_KEY_SAML_SIGNATURE = "Signature" -SESSION_KEY_SAML_SIG_ALG = "SigAlg" -SESSION_KEY_SAML_RESPONSE = "SAMLResponse" -SESSION_KEY_RELAY_STATE = "RelayState" +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" + SESSION_KEY_AUTH_N_REQUEST = "authn_request" @@ -96,8 +97,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView): self, request: HttpRequest, application_slug: str ) -> Optional[HttpResponse]: """Handle REDIRECT bindings""" - # Store these values now, because Django's login cycle won't preserve them. - if SESSION_KEY_SAML_REQUEST not in request.GET: + if REQUEST_KEY_SAML_REQUEST not in request.GET: LOGGER.info("handle_saml_request: SAML payload missing") return bad_request_message( self.request, "The SAML request payload is missing." @@ -105,10 +105,10 @@ class SAMLSSOBindingRedirectView(SAMLSSOView): try: auth_n_request = AuthNRequestParser(self.provider).parse_detached( - request.GET[SESSION_KEY_SAML_REQUEST], - request.GET.get(SESSION_KEY_RELAY_STATE, ""), - request.GET.get(SESSION_KEY_SAML_SIGNATURE), - request.GET.get(SESSION_KEY_SAML_SIG_ALG), + request.GET[REQUEST_KEY_SAML_REQUEST], + request.GET.get(REQUEST_KEY_RELAY_STATE), + request.GET.get(REQUEST_KEY_SAML_SIGNATURE), + request.GET.get(REQUEST_KEY_SAML_SIG_ALG), ) self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request except CannotHandleAssertion as exc: @@ -126,8 +126,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView): self, request: HttpRequest, application_slug: str ) -> Optional[HttpResponse]: """Handle POST bindings""" - # Store these values now, because Django's login cycle won't preserve them. - if SESSION_KEY_SAML_REQUEST not in request.POST: + if REQUEST_KEY_SAML_REQUEST not in request.POST: LOGGER.info("handle_saml_request: SAML payload missing") return bad_request_message( self.request, "The SAML request payload is missing." @@ -135,8 +134,8 @@ class SAMLSSOBindingPOSTView(SAMLSSOView): try: auth_n_request = AuthNRequestParser(self.provider).parse( - request.POST[SESSION_KEY_SAML_REQUEST], - request.POST.get(SESSION_KEY_RELAY_STATE, ""), + request.POST[REQUEST_KEY_SAML_REQUEST], + request.POST.get(REQUEST_KEY_RELAY_STATE), ) self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request except CannotHandleAssertion as exc: @@ -177,11 +176,10 @@ class SAMLFlowFinalView(StageView): authorized_application=application, flow=self.executor.plan.flow_pk, ).from_http(self.request) - self.request.session.pop(SESSION_KEY_SAML_REQUEST, None) - self.request.session.pop(SESSION_KEY_SAML_RESPONSE, None) - self.request.session.pop(SESSION_KEY_RELAY_STATE, None) + if SESSION_KEY_AUTH_N_REQUEST not in self.request.session: return self.executor.stage_invalid() + auth_n_request: AuthNRequest = self.request.session.pop( SESSION_KEY_AUTH_N_REQUEST ) @@ -198,16 +196,16 @@ class SAMLFlowFinalView(StageView): "title": _("Redirecting to %(app)s..." % {"app": application.name}), "attrs": { "ACSUrl": provider.acs_url, - SESSION_KEY_SAML_RESPONSE: nice64(response.encode()), - SESSION_KEY_RELAY_STATE: auth_n_request.relay_state, + REQUEST_KEY_SAML_RESPONSE: nice64(response.encode()), + REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state, }, }, ) if provider.sp_binding == SAMLBindings.REDIRECT: querystring = urlencode( { - SESSION_KEY_SAML_RESPONSE: nice64(response.encode()), - SESSION_KEY_RELAY_STATE: auth_n_request.relay_state, + REQUEST_KEY_SAML_RESPONSE: nice64(response.encode()), + REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state, } ) return redirect(f"{provider.acs_url}?{querystring}")