providers/saml: Generate NameID Value based on NameID Policy received

This commit is contained in:
Jens Langhammer 2020-07-12 17:06:15 +02:00
parent f8e5383ba2
commit d1151091cd
3 changed files with 51 additions and 29 deletions

View File

@ -1,4 +1,5 @@
"""SAML Assertion generator""" """SAML Assertion generator"""
from hashlib import sha256
from types import GeneratorType from types import GeneratorType
from django.http import HttpRequest 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.processors.request_parser import AuthNRequest
from passbook.providers.saml.utils import get_random_id from passbook.providers.saml.utils import get_random_id
from passbook.providers.saml.utils.time import get_time_string, timedelta_from_string 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 ( from passbook.sources.saml.processors.constants import (
NS_MAP, NS_MAP,
NS_SAML_ASSERTION, NS_SAML_ASSERTION,
NS_SAML_PROTOCOL, NS_SAML_PROTOCOL,
NS_SIGNATURE, NS_SIGNATURE,
SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_PRESISTENT,
SAML_NAME_ID_FORMAT_TRANSIENT,
SAML_NAME_ID_FORMAT_X509,
) )
LOGGER = get_logger() LOGGER = get_logger()
@ -127,14 +132,35 @@ class AssertionProcessor:
audience.text = self.provider.audience audience.text = self.provider.audience
return conditions 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: def get_assertion_subject(self) -> Element:
"""Generate Subject Element with NameID and SubjectConfirmation Objects""" """Generate Subject Element with NameID and SubjectConfirmation Objects"""
subject = Element(f"{{{NS_SAML_ASSERTION}}}Subject") subject = Element(f"{{{NS_SAML_ASSERTION}}}Subject")
subject.append(self.get_name_id())
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_confirmation = SubElement( subject_confirmation = SubElement(
subject, f"{{{NS_SAML_ASSERTION}}}SubjectConfirmation" subject, f"{{{NS_SAML_ASSERTION}}}SubjectConfirmation"

View File

@ -17,7 +17,6 @@ from passbook.sources.saml.processors.constants import (
SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_PRESISTENT, SAML_NAME_ID_FORMAT_PRESISTENT,
SAML_NAME_ID_FORMAT_TRANSIENT, SAML_NAME_ID_FORMAT_TRANSIENT,
SAML_NAME_ID_FORMAT_WINDOWS,
SAML_NAME_ID_FORMAT_X509, SAML_NAME_ID_FORMAT_X509,
) )
@ -54,7 +53,6 @@ class MetadataProcessor:
SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_PRESISTENT, SAML_NAME_ID_FORMAT_PRESISTENT,
SAML_NAME_ID_FORMAT_X509, SAML_NAME_ID_FORMAT_X509,
SAML_NAME_ID_FORMAT_WINDOWS,
SAML_NAME_ID_FORMAT_TRANSIENT, SAML_NAME_ID_FORMAT_TRANSIENT,
] ]
for name_id_format in formats: for name_id_format in formats:

View File

@ -38,11 +38,12 @@ from passbook.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
LOGGER = get_logger() LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https")) URL_VALIDATOR = URLValidator(schemes=("http", "https"))
SESSION_KEY_SAML_REQUEST = "SAMLRequest" REQUEST_KEY_SAML_REQUEST = "SAMLRequest"
SESSION_KEY_SAML_SIGNATURE = "Signature" REQUEST_KEY_SAML_SIGNATURE = "Signature"
SESSION_KEY_SAML_SIG_ALG = "SigAlg" REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
SESSION_KEY_SAML_RESPONSE = "SAMLResponse" REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
SESSION_KEY_RELAY_STATE = "RelayState" REQUEST_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_AUTH_N_REQUEST = "authn_request" SESSION_KEY_AUTH_N_REQUEST = "authn_request"
@ -96,8 +97,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
self, request: HttpRequest, application_slug: str self, request: HttpRequest, application_slug: str
) -> Optional[HttpResponse]: ) -> Optional[HttpResponse]:
"""Handle REDIRECT bindings""" """Handle REDIRECT bindings"""
# Store these values now, because Django's login cycle won't preserve them. if REQUEST_KEY_SAML_REQUEST not in request.GET:
if SESSION_KEY_SAML_REQUEST not in request.GET:
LOGGER.info("handle_saml_request: SAML payload missing") LOGGER.info("handle_saml_request: SAML payload missing")
return bad_request_message( return bad_request_message(
self.request, "The SAML request payload is missing." self.request, "The SAML request payload is missing."
@ -105,10 +105,10 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
try: try:
auth_n_request = AuthNRequestParser(self.provider).parse_detached( auth_n_request = AuthNRequestParser(self.provider).parse_detached(
request.GET[SESSION_KEY_SAML_REQUEST], request.GET[REQUEST_KEY_SAML_REQUEST],
request.GET.get(SESSION_KEY_RELAY_STATE, ""), request.GET.get(REQUEST_KEY_RELAY_STATE),
request.GET.get(SESSION_KEY_SAML_SIGNATURE), request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
request.GET.get(SESSION_KEY_SAML_SIG_ALG), request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
) )
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:
@ -126,8 +126,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
self, request: HttpRequest, application_slug: str self, request: HttpRequest, application_slug: str
) -> Optional[HttpResponse]: ) -> Optional[HttpResponse]:
"""Handle POST bindings""" """Handle POST bindings"""
# Store these values now, because Django's login cycle won't preserve them. if REQUEST_KEY_SAML_REQUEST not in request.POST:
if SESSION_KEY_SAML_REQUEST not in request.POST:
LOGGER.info("handle_saml_request: SAML payload missing") LOGGER.info("handle_saml_request: SAML payload missing")
return bad_request_message( return bad_request_message(
self.request, "The SAML request payload is missing." self.request, "The SAML request payload is missing."
@ -135,8 +134,8 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
try: try:
auth_n_request = AuthNRequestParser(self.provider).parse( auth_n_request = AuthNRequestParser(self.provider).parse(
request.POST[SESSION_KEY_SAML_REQUEST], request.POST[REQUEST_KEY_SAML_REQUEST],
request.POST.get(SESSION_KEY_RELAY_STATE, ""), request.POST.get(REQUEST_KEY_RELAY_STATE),
) )
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:
@ -177,11 +176,10 @@ class SAMLFlowFinalView(StageView):
authorized_application=application, authorized_application=application,
flow=self.executor.plan.flow_pk, flow=self.executor.plan.flow_pk,
).from_http(self.request) ).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: if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
return self.executor.stage_invalid() return self.executor.stage_invalid()
auth_n_request: AuthNRequest = self.request.session.pop( auth_n_request: AuthNRequest = self.request.session.pop(
SESSION_KEY_AUTH_N_REQUEST SESSION_KEY_AUTH_N_REQUEST
) )
@ -198,16 +196,16 @@ class SAMLFlowFinalView(StageView):
"title": _("Redirecting to %(app)s..." % {"app": application.name}), "title": _("Redirecting to %(app)s..." % {"app": application.name}),
"attrs": { "attrs": {
"ACSUrl": provider.acs_url, "ACSUrl": provider.acs_url,
SESSION_KEY_SAML_RESPONSE: nice64(response.encode()), REQUEST_KEY_SAML_RESPONSE: nice64(response.encode()),
SESSION_KEY_RELAY_STATE: auth_n_request.relay_state, REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
}, },
}, },
) )
if provider.sp_binding == SAMLBindings.REDIRECT: if provider.sp_binding == SAMLBindings.REDIRECT:
querystring = urlencode( querystring = urlencode(
{ {
SESSION_KEY_SAML_RESPONSE: nice64(response.encode()), REQUEST_KEY_SAML_RESPONSE: nice64(response.encode()),
SESSION_KEY_RELAY_STATE: auth_n_request.relay_state, REQUEST_KEY_RELAY_STATE: auth_n_request.relay_state,
} }
) )
return redirect(f"{provider.acs_url}?{querystring}") return redirect(f"{provider.acs_url}?{querystring}")