providers/saml: Generate NameID Value based on NameID Policy received
This commit is contained in:
parent
f8e5383ba2
commit
d1151091cd
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
Reference in a new issue