authentik.providers.saml.processors.metadata
SAML Identity Provider Metadata Processor
1"""SAML Identity Provider Metadata Processor""" 2 3from collections.abc import Iterator 4from hashlib import sha256 5 6import xmlsec # nosec 7from django.http import HttpRequest 8from django.urls import reverse 9from lxml.etree import Element, SubElement, _Element, tostring # nosec 10 11from authentik.common.saml.constants import ( 12 DIGEST_ALGORITHM_TRANSLATION_MAP, 13 NS_MAP, 14 NS_SAML_METADATA, 15 NS_SAML_PROTOCOL, 16 NS_SIGNATURE, 17 SAML_BINDING_POST, 18 SAML_BINDING_REDIRECT, 19 SAML_NAME_ID_FORMAT_EMAIL, 20 SAML_NAME_ID_FORMAT_PERSISTENT, 21 SAML_NAME_ID_FORMAT_TRANSIENT, 22 SAML_NAME_ID_FORMAT_X509, 23 SIGN_ALGORITHM_TRANSFORM_MAP, 24) 25from authentik.lib.xml import remove_xml_newlines 26from authentik.providers.saml.models import SAMLProvider 27from authentik.providers.saml.utils.encoding import strip_pem_header 28 29 30class MetadataProcessor: 31 """SAML Identity Provider Metadata Processor""" 32 33 provider: SAMLProvider 34 http_request: HttpRequest 35 force_binding: str | None 36 37 def __init__(self, provider: SAMLProvider, request: HttpRequest): 38 self.provider = provider 39 self.http_request = request 40 self.force_binding = None 41 self.xml_id = "_" + sha256(f"{provider.name}-{provider.pk}".encode("ascii")).hexdigest() 42 43 def _get_issuer_value(self) -> str: 44 """Get issuer value, with fallback to generated URL if empty""" 45 # If user has set an override issuer, use it 46 if self.provider.issuer_override: 47 return self.provider.issuer_override 48 49 return self.http_request.build_absolute_uri( 50 reverse( 51 "authentik_providers_saml:metadata-download", 52 kwargs={"application_slug": self.provider.application.slug}, 53 ) 54 ) 55 56 # Using type unions doesn't work with cython types (which is what lxml is) 57 def get_signing_key_descriptor(self) -> Element | None: 58 """Get Signing KeyDescriptor, if enabled for the provider""" 59 if not self.provider.signing_kp: 60 return None 61 key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") 62 key_descriptor.attrib["use"] = "signing" 63 key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo") 64 x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data") 65 x509_certificate = SubElement(x509_data, f"{{{NS_SIGNATURE}}}X509Certificate") 66 x509_certificate.text = strip_pem_header( 67 self.provider.signing_kp.certificate_data.replace("\r", "") 68 ) 69 return key_descriptor 70 71 def get_name_id_formats(self) -> Iterator[Element]: 72 """Get compatible NameID Formats""" 73 formats = [ 74 SAML_NAME_ID_FORMAT_EMAIL, 75 SAML_NAME_ID_FORMAT_PERSISTENT, 76 SAML_NAME_ID_FORMAT_X509, 77 SAML_NAME_ID_FORMAT_TRANSIENT, 78 ] 79 for name_id_format in formats: 80 element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat") 81 element.text = name_id_format 82 yield element 83 84 def _get_unified_url(self) -> str: 85 """Get the unified SAML endpoint URL""" 86 return self.http_request.build_absolute_uri( 87 reverse( 88 "authentik_providers_saml:base", 89 kwargs={"application_slug": self.provider.application.slug}, 90 ) 91 ) 92 93 def get_sso_bindings(self) -> Iterator[Element]: 94 """Get all SSO Bindings - both point to unified endpoint""" 95 unified_url = self._get_unified_url() 96 for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]: 97 if self.force_binding and self.force_binding != binding: 98 continue 99 element = Element(f"{{{NS_SAML_METADATA}}}SingleSignOnService") 100 element.attrib["Binding"] = binding 101 element.attrib["Location"] = unified_url 102 yield element 103 104 def get_slo_bindings(self) -> Iterator[Element]: 105 """Get all SLO Bindings - both point to unified endpoint""" 106 unified_url = self._get_unified_url() 107 for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]: 108 if self.force_binding and self.force_binding != binding: 109 continue 110 element = Element(f"{{{NS_SAML_METADATA}}}SingleLogoutService") 111 element.attrib["Binding"] = binding 112 element.attrib["Location"] = unified_url 113 yield element 114 115 def _prepare_signature(self, entity_descriptor: _Element): 116 sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get( 117 self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1 118 ) 119 signature = xmlsec.template.create( 120 entity_descriptor, 121 xmlsec.constants.TransformExclC14N, 122 sign_algorithm_transform, 123 ns=xmlsec.constants.DSigNs, 124 ) 125 entity_descriptor.append(signature) 126 127 def _sign(self, entity_descriptor: _Element): 128 digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get( 129 self.provider.digest_algorithm, xmlsec.constants.TransformSha1 130 ) 131 assertion = entity_descriptor.xpath("//md:EntityDescriptor", namespaces=NS_MAP)[0] 132 xmlsec.tree.add_ids(assertion, ["ID"]) 133 signature_node = xmlsec.tree.find_node(assertion, xmlsec.constants.NodeSignature) 134 ref = xmlsec.template.add_reference( 135 signature_node, 136 digest_algorithm_transform, 137 uri="#" + self.xml_id, 138 ) 139 xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped) 140 xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N) 141 key_info = xmlsec.template.ensure_key_info(signature_node) 142 xmlsec.template.add_x509_data(key_info) 143 144 ctx = xmlsec.SignatureContext() 145 146 key = xmlsec.Key.from_memory( 147 self.provider.signing_kp.key_data, 148 xmlsec.constants.KeyDataFormatPem, 149 None, 150 ) 151 key.load_cert_from_memory( 152 self.provider.signing_kp.certificate_data, 153 xmlsec.constants.KeyDataFormatCertPem, 154 ) 155 ctx.key = key 156 ctx.sign(remove_xml_newlines(assertion, signature_node)) 157 158 def add_children(self, entity_descriptor: _Element): 159 self.add_idp_sso(entity_descriptor) 160 161 def add_idp_sso(self, entity_descriptor: _Element): 162 idp_sso_descriptor = SubElement( 163 entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor" 164 ) 165 idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL 166 if self.provider.verification_kp: 167 idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true" 168 169 signing_descriptor = self.get_signing_key_descriptor() 170 if signing_descriptor is not None: 171 idp_sso_descriptor.append(signing_descriptor) 172 173 for binding in self.get_slo_bindings(): 174 idp_sso_descriptor.append(binding) 175 176 for name_id_format in self.get_name_id_formats(): 177 idp_sso_descriptor.append(name_id_format) 178 179 for binding in self.get_sso_bindings(): 180 idp_sso_descriptor.append(binding) 181 182 def build_entity_descriptor(self) -> str: 183 """Build full EntityDescriptor""" 184 entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) 185 entity_descriptor.attrib["ID"] = self.xml_id 186 entity_descriptor.attrib["entityID"] = self._get_issuer_value() 187 188 if self.provider.signing_kp: 189 self._prepare_signature(entity_descriptor) 190 191 self.add_children(entity_descriptor) 192 193 if self.provider.signing_kp: 194 self._sign(entity_descriptor) 195 196 return tostring(entity_descriptor).decode()
class
MetadataProcessor:
31class MetadataProcessor: 32 """SAML Identity Provider Metadata Processor""" 33 34 provider: SAMLProvider 35 http_request: HttpRequest 36 force_binding: str | None 37 38 def __init__(self, provider: SAMLProvider, request: HttpRequest): 39 self.provider = provider 40 self.http_request = request 41 self.force_binding = None 42 self.xml_id = "_" + sha256(f"{provider.name}-{provider.pk}".encode("ascii")).hexdigest() 43 44 def _get_issuer_value(self) -> str: 45 """Get issuer value, with fallback to generated URL if empty""" 46 # If user has set an override issuer, use it 47 if self.provider.issuer_override: 48 return self.provider.issuer_override 49 50 return self.http_request.build_absolute_uri( 51 reverse( 52 "authentik_providers_saml:metadata-download", 53 kwargs={"application_slug": self.provider.application.slug}, 54 ) 55 ) 56 57 # Using type unions doesn't work with cython types (which is what lxml is) 58 def get_signing_key_descriptor(self) -> Element | None: 59 """Get Signing KeyDescriptor, if enabled for the provider""" 60 if not self.provider.signing_kp: 61 return None 62 key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") 63 key_descriptor.attrib["use"] = "signing" 64 key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo") 65 x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data") 66 x509_certificate = SubElement(x509_data, f"{{{NS_SIGNATURE}}}X509Certificate") 67 x509_certificate.text = strip_pem_header( 68 self.provider.signing_kp.certificate_data.replace("\r", "") 69 ) 70 return key_descriptor 71 72 def get_name_id_formats(self) -> Iterator[Element]: 73 """Get compatible NameID Formats""" 74 formats = [ 75 SAML_NAME_ID_FORMAT_EMAIL, 76 SAML_NAME_ID_FORMAT_PERSISTENT, 77 SAML_NAME_ID_FORMAT_X509, 78 SAML_NAME_ID_FORMAT_TRANSIENT, 79 ] 80 for name_id_format in formats: 81 element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat") 82 element.text = name_id_format 83 yield element 84 85 def _get_unified_url(self) -> str: 86 """Get the unified SAML endpoint URL""" 87 return self.http_request.build_absolute_uri( 88 reverse( 89 "authentik_providers_saml:base", 90 kwargs={"application_slug": self.provider.application.slug}, 91 ) 92 ) 93 94 def get_sso_bindings(self) -> Iterator[Element]: 95 """Get all SSO Bindings - both point to unified endpoint""" 96 unified_url = self._get_unified_url() 97 for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]: 98 if self.force_binding and self.force_binding != binding: 99 continue 100 element = Element(f"{{{NS_SAML_METADATA}}}SingleSignOnService") 101 element.attrib["Binding"] = binding 102 element.attrib["Location"] = unified_url 103 yield element 104 105 def get_slo_bindings(self) -> Iterator[Element]: 106 """Get all SLO Bindings - both point to unified endpoint""" 107 unified_url = self._get_unified_url() 108 for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]: 109 if self.force_binding and self.force_binding != binding: 110 continue 111 element = Element(f"{{{NS_SAML_METADATA}}}SingleLogoutService") 112 element.attrib["Binding"] = binding 113 element.attrib["Location"] = unified_url 114 yield element 115 116 def _prepare_signature(self, entity_descriptor: _Element): 117 sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get( 118 self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1 119 ) 120 signature = xmlsec.template.create( 121 entity_descriptor, 122 xmlsec.constants.TransformExclC14N, 123 sign_algorithm_transform, 124 ns=xmlsec.constants.DSigNs, 125 ) 126 entity_descriptor.append(signature) 127 128 def _sign(self, entity_descriptor: _Element): 129 digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get( 130 self.provider.digest_algorithm, xmlsec.constants.TransformSha1 131 ) 132 assertion = entity_descriptor.xpath("//md:EntityDescriptor", namespaces=NS_MAP)[0] 133 xmlsec.tree.add_ids(assertion, ["ID"]) 134 signature_node = xmlsec.tree.find_node(assertion, xmlsec.constants.NodeSignature) 135 ref = xmlsec.template.add_reference( 136 signature_node, 137 digest_algorithm_transform, 138 uri="#" + self.xml_id, 139 ) 140 xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped) 141 xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N) 142 key_info = xmlsec.template.ensure_key_info(signature_node) 143 xmlsec.template.add_x509_data(key_info) 144 145 ctx = xmlsec.SignatureContext() 146 147 key = xmlsec.Key.from_memory( 148 self.provider.signing_kp.key_data, 149 xmlsec.constants.KeyDataFormatPem, 150 None, 151 ) 152 key.load_cert_from_memory( 153 self.provider.signing_kp.certificate_data, 154 xmlsec.constants.KeyDataFormatCertPem, 155 ) 156 ctx.key = key 157 ctx.sign(remove_xml_newlines(assertion, signature_node)) 158 159 def add_children(self, entity_descriptor: _Element): 160 self.add_idp_sso(entity_descriptor) 161 162 def add_idp_sso(self, entity_descriptor: _Element): 163 idp_sso_descriptor = SubElement( 164 entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor" 165 ) 166 idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL 167 if self.provider.verification_kp: 168 idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true" 169 170 signing_descriptor = self.get_signing_key_descriptor() 171 if signing_descriptor is not None: 172 idp_sso_descriptor.append(signing_descriptor) 173 174 for binding in self.get_slo_bindings(): 175 idp_sso_descriptor.append(binding) 176 177 for name_id_format in self.get_name_id_formats(): 178 idp_sso_descriptor.append(name_id_format) 179 180 for binding in self.get_sso_bindings(): 181 idp_sso_descriptor.append(binding) 182 183 def build_entity_descriptor(self) -> str: 184 """Build full EntityDescriptor""" 185 entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) 186 entity_descriptor.attrib["ID"] = self.xml_id 187 entity_descriptor.attrib["entityID"] = self._get_issuer_value() 188 189 if self.provider.signing_kp: 190 self._prepare_signature(entity_descriptor) 191 192 self.add_children(entity_descriptor) 193 194 if self.provider.signing_kp: 195 self._sign(entity_descriptor) 196 197 return tostring(entity_descriptor).decode()
SAML Identity Provider Metadata Processor
MetadataProcessor( provider: authentik.providers.saml.models.SAMLProvider, request: django.http.request.HttpRequest)
def
get_signing_key_descriptor(self) -> lxml.etree.Element | None:
58 def get_signing_key_descriptor(self) -> Element | None: 59 """Get Signing KeyDescriptor, if enabled for the provider""" 60 if not self.provider.signing_kp: 61 return None 62 key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") 63 key_descriptor.attrib["use"] = "signing" 64 key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo") 65 x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data") 66 x509_certificate = SubElement(x509_data, f"{{{NS_SIGNATURE}}}X509Certificate") 67 x509_certificate.text = strip_pem_header( 68 self.provider.signing_kp.certificate_data.replace("\r", "") 69 ) 70 return key_descriptor
Get Signing KeyDescriptor, if enabled for the provider
def
get_name_id_formats(self) -> Iterator[lxml.etree.Element]:
72 def get_name_id_formats(self) -> Iterator[Element]: 73 """Get compatible NameID Formats""" 74 formats = [ 75 SAML_NAME_ID_FORMAT_EMAIL, 76 SAML_NAME_ID_FORMAT_PERSISTENT, 77 SAML_NAME_ID_FORMAT_X509, 78 SAML_NAME_ID_FORMAT_TRANSIENT, 79 ] 80 for name_id_format in formats: 81 element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat") 82 element.text = name_id_format 83 yield element
Get compatible NameID Formats
def
get_sso_bindings(self) -> Iterator[lxml.etree.Element]:
94 def get_sso_bindings(self) -> Iterator[Element]: 95 """Get all SSO Bindings - both point to unified endpoint""" 96 unified_url = self._get_unified_url() 97 for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]: 98 if self.force_binding and self.force_binding != binding: 99 continue 100 element = Element(f"{{{NS_SAML_METADATA}}}SingleSignOnService") 101 element.attrib["Binding"] = binding 102 element.attrib["Location"] = unified_url 103 yield element
Get all SSO Bindings - both point to unified endpoint
def
get_slo_bindings(self) -> Iterator[lxml.etree.Element]:
105 def get_slo_bindings(self) -> Iterator[Element]: 106 """Get all SLO Bindings - both point to unified endpoint""" 107 unified_url = self._get_unified_url() 108 for binding in [SAML_BINDING_REDIRECT, SAML_BINDING_POST]: 109 if self.force_binding and self.force_binding != binding: 110 continue 111 element = Element(f"{{{NS_SAML_METADATA}}}SingleLogoutService") 112 element.attrib["Binding"] = binding 113 element.attrib["Location"] = unified_url 114 yield element
Get all SLO Bindings - both point to unified endpoint
def
add_idp_sso(self, entity_descriptor: lxml.etree._Element):
162 def add_idp_sso(self, entity_descriptor: _Element): 163 idp_sso_descriptor = SubElement( 164 entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor" 165 ) 166 idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL 167 if self.provider.verification_kp: 168 idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true" 169 170 signing_descriptor = self.get_signing_key_descriptor() 171 if signing_descriptor is not None: 172 idp_sso_descriptor.append(signing_descriptor) 173 174 for binding in self.get_slo_bindings(): 175 idp_sso_descriptor.append(binding) 176 177 for name_id_format in self.get_name_id_formats(): 178 idp_sso_descriptor.append(name_id_format) 179 180 for binding in self.get_sso_bindings(): 181 idp_sso_descriptor.append(binding)
def
build_entity_descriptor(self) -> str:
183 def build_entity_descriptor(self) -> str: 184 """Build full EntityDescriptor""" 185 entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) 186 entity_descriptor.attrib["ID"] = self.xml_id 187 entity_descriptor.attrib["entityID"] = self._get_issuer_value() 188 189 if self.provider.signing_kp: 190 self._prepare_signature(entity_descriptor) 191 192 self.add_children(entity_descriptor) 193 194 if self.provider.signing_kp: 195 self._sign(entity_descriptor) 196 197 return tostring(entity_descriptor).decode()
Build full EntityDescriptor