sources/saml: rewrite Processors and Views to directly build XML without templates
This commit is contained in:
@ -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"
|
||||
|
94
passbook/sources/saml/processors/metadata.py
Normal file
94
passbook/sources/saml/processors/metadata.py
Normal 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()
|
53
passbook/sources/saml/processors/request.py
Normal file
53
passbook/sources/saml/processors/request.py
Normal 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()
|
@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
||||
DEFAULT_BACKEND = "django.contrib.auth.backends.ModelBackend"
|
||||
|
||||
|
||||
class Processor:
|
||||
class ResponseProcessor:
|
||||
"""SAML Response Processor"""
|
||||
|
||||
_source: SAMLSource
|
Reference in New Issue
Block a user