sources/saml: rewrite Processors and Views to directly build XML without templates

This commit is contained in:
Jens Langhammer
2020-07-11 01:02:55 +02:00
parent 1e31cd03ed
commit 92a09be8c0
13 changed files with 193 additions and 138 deletions

View File

@ -1,4 +1,16 @@
"""SAML Source processor constants"""
NS_SAML_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
NS_SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
NS_SAML_METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
NS_SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
NS_MAP = {
"samlp": NS_SAML_PROTOCOL,
"saml": NS_SAML_ASSERTION,
"ds": NS_SIGNATURE,
"md": NS_SAML_METADATA,
}
SAML_NAME_ID_FORMAT_EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
SAML_NAME_ID_FORMAT_PRESISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
SAML_NAME_ID_FORMAT_X509 = "urn:oasis:names:tc:SAML:2.0:nameid-format:X509SubjectName"
@ -6,3 +18,6 @@ SAML_NAME_ID_FORMAT_WINDOWS = (
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName"
)
SAML_NAME_ID_FORMAT_TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
SAML_BINDING_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"

View File

@ -0,0 +1,94 @@
"""SAML Service Provider Metadata Processor"""
from typing import Iterator, Optional
from defusedxml import ElementTree
from django.http import HttpRequest
from lxml.etree import Element, SubElement # nosec
from signxml.util import strip_pem_header
from passbook.sources.saml.models import SAMLSource
from passbook.sources.saml.processors.constants import (
NS_MAP,
NS_SAML_METADATA,
NS_SIGNATURE,
SAML_BINDING_POST,
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,
)
class MetadataProcessor:
"""SAML Service Provider Metadata Processor"""
source: SAMLSource
http_request: HttpRequest
def __init__(self, source: SAMLSource, request: HttpRequest):
self.source = source
self.http_request = request
def get_signing_key_descriptor(self) -> Optional[Element]:
"""Get Singing KeyDescriptor, if enabled for the source"""
if self.source.signing_kp:
key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor")
key_descriptor.attrib["use"] = "signing"
key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo")
x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data")
x509_certificate = SubElement(
x509_data, f"{{{NS_SIGNATURE}}}X509Certificate"
)
x509_certificate.text = strip_pem_header(
self.source.signing_kp.certificate_data.replace("\r", "")
).replace("\n", "")
return key_descriptor
return None
def get_name_id_formats(self) -> Iterator[Element]:
"""Get compatible NameID Formats"""
formats = [
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:
element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat")
element.text = name_id_format
yield element
def build_entity_descriptor(self) -> str:
"""Build full EntityDescriptor"""
entity_descriptor = Element(
f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP
)
entity_descriptor.attrib["entityID"] = self.source.get_issuer(self.http_request)
sp_sso_descriptor = SubElement(
entity_descriptor, f"{{{NS_SAML_METADATA}}}SPSSODescriptor"
)
sp_sso_descriptor.attrib[
"protocolSupportEnumeration"
] = "urn:oasis:names:tc:SAML:2.0:protocol"
signing_descriptor = self.get_signing_key_descriptor()
if signing_descriptor:
sp_sso_descriptor.append(signing_descriptor)
for name_id_format in self.get_name_id_formats():
sp_sso_descriptor.append(name_id_format)
assertion_consumer_service = SubElement(
sp_sso_descriptor, f"{{{NS_SAML_METADATA}}}"
)
assertion_consumer_service.attrib["isDefault"] = True
assertion_consumer_service.attrib["index"] = 0
assertion_consumer_service.attrib["Binding"] = SAML_BINDING_POST
assertion_consumer_service.attrib["Location"] = self.source.build_full_url(
self.http_request
)
return ElementTree.tostring(entity_descriptor).decode()

View File

@ -0,0 +1,53 @@
"""SAML AuthnRequest Processor"""
from defusedxml import ElementTree
from django.http import HttpRequest
from lxml.etree import Element # nosec
from passbook.providers.saml.utils import get_random_id
from passbook.providers.saml.utils.time import get_time_string
from passbook.sources.saml.models import SAMLSource
from passbook.sources.saml.processors.constants import (
NS_MAP,
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
)
class RequestProcessor:
"""SAML AuthnRequest Processor"""
source: SAMLSource
http_request: HttpRequest
def __init__(self, source: SAMLSource, request: HttpRequest):
self.source = source
self.http_request = request
def get_issuer(self) -> Element:
"""Get Issuer Element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self.source.get_issuer(self.http_request)
return issuer
def get_name_id_policy(self) -> Element:
"""Get NameID Policy Element"""
name_id_policy = Element(f"{{{NS_SAML_PROTOCOL}}}NameIDPolicy")
name_id_policy.text = self.source.name_id_policy
return name_id_policy
def build_auth_n(self) -> str:
"""Get full AuthnRequest"""
auth_n_request = Element(f"{{{NS_SAML_PROTOCOL}}}AuthnRequest", nsmap=NS_MAP)
auth_n_request.attrib[
"AssertionConsumerServiceURL"
] = self.source.build_full_url(self.http_request)
auth_n_request.attrib["Destination"] = self.source.sso_url
auth_n_request.attrib["ID"] = get_random_id()
auth_n_request.attrib["IssueInstant"] = get_time_string()
auth_n_request.attrib["ProtocolBinding"] = self.source.binding_type
auth_n_request.attrib["Version"] = "2.0"
# Create issuer object
auth_n_request.append(self.get_issuer())
# Create NameID Policy Object
auth_n_request.append(self.get_name_id_policy())
return ElementTree.tostring(auth_n_request).decode()

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
DEFAULT_BACKEND = "django.contrib.auth.backends.ModelBackend"
class Processor:
class ResponseProcessor:
"""SAML Response Processor"""
_source: SAMLSource