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 # Using type unions doesn't work with cython types (which is what lxml is) 44 def get_signing_key_descriptor(self) -> Element | None: 45 """Get Signing KeyDescriptor, if enabled for the provider""" 46 if not self.provider.signing_kp: 47 return None 48 key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") 49 key_descriptor.attrib["use"] = "signing" 50 key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo") 51 x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data") 52 x509_certificate = SubElement(x509_data, f"{{{NS_SIGNATURE}}}X509Certificate") 53 x509_certificate.text = strip_pem_header( 54 self.provider.signing_kp.certificate_data.replace("\r", "") 55 ) 56 return key_descriptor 57 58 def get_name_id_formats(self) -> Iterator[Element]: 59 """Get compatible NameID Formats""" 60 formats = [ 61 SAML_NAME_ID_FORMAT_EMAIL, 62 SAML_NAME_ID_FORMAT_PERSISTENT, 63 SAML_NAME_ID_FORMAT_X509, 64 SAML_NAME_ID_FORMAT_TRANSIENT, 65 ] 66 for name_id_format in formats: 67 element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat") 68 element.text = name_id_format 69 yield element 70 71 def get_sso_bindings(self) -> Iterator[Element]: 72 """Get all Bindings supported""" 73 binding_url_map = { 74 (SAML_BINDING_REDIRECT, "SingleSignOnService"): self.http_request.build_absolute_uri( 75 reverse( 76 "authentik_providers_saml:sso-redirect", 77 kwargs={"application_slug": self.provider.application.slug}, 78 ) 79 ), 80 (SAML_BINDING_POST, "SingleSignOnService"): self.http_request.build_absolute_uri( 81 reverse( 82 "authentik_providers_saml:sso-post", 83 kwargs={"application_slug": self.provider.application.slug}, 84 ) 85 ), 86 } 87 for binding_svc, url in binding_url_map.items(): 88 binding, svc = binding_svc 89 if self.force_binding and self.force_binding != binding: 90 continue 91 element = Element(f"{{{NS_SAML_METADATA}}}{svc}") 92 element.attrib["Binding"] = binding 93 element.attrib["Location"] = url 94 yield element 95 96 def get_slo_bindings(self) -> Iterator[Element]: 97 """Get all Bindings supported""" 98 binding_url_map = { 99 (SAML_BINDING_REDIRECT, "SingleLogoutService"): self.http_request.build_absolute_uri( 100 reverse( 101 "authentik_providers_saml:slo-redirect", 102 kwargs={"application_slug": self.provider.application.slug}, 103 ) 104 ), 105 (SAML_BINDING_POST, "SingleLogoutService"): self.http_request.build_absolute_uri( 106 reverse( 107 "authentik_providers_saml:slo-post", 108 kwargs={"application_slug": self.provider.application.slug}, 109 ) 110 ), 111 } 112 for binding_svc, url in binding_url_map.items(): 113 binding, svc = binding_svc 114 if self.force_binding and self.force_binding != binding: 115 continue 116 element = Element(f"{{{NS_SAML_METADATA}}}{svc}") 117 element.attrib["Binding"] = binding 118 element.attrib["Location"] = url 119 yield element 120 121 def _prepare_signature(self, entity_descriptor: _Element): 122 sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get( 123 self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1 124 ) 125 signature = xmlsec.template.create( 126 entity_descriptor, 127 xmlsec.constants.TransformExclC14N, 128 sign_algorithm_transform, 129 ns=xmlsec.constants.DSigNs, 130 ) 131 entity_descriptor.append(signature) 132 133 def _sign(self, entity_descriptor: _Element): 134 digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get( 135 self.provider.digest_algorithm, xmlsec.constants.TransformSha1 136 ) 137 assertion = entity_descriptor.xpath("//md:EntityDescriptor", namespaces=NS_MAP)[0] 138 xmlsec.tree.add_ids(assertion, ["ID"]) 139 signature_node = xmlsec.tree.find_node(assertion, xmlsec.constants.NodeSignature) 140 ref = xmlsec.template.add_reference( 141 signature_node, 142 digest_algorithm_transform, 143 uri="#" + self.xml_id, 144 ) 145 xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped) 146 xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N) 147 key_info = xmlsec.template.ensure_key_info(signature_node) 148 xmlsec.template.add_x509_data(key_info) 149 150 ctx = xmlsec.SignatureContext() 151 152 key = xmlsec.Key.from_memory( 153 self.provider.signing_kp.key_data, 154 xmlsec.constants.KeyDataFormatPem, 155 None, 156 ) 157 key.load_cert_from_memory( 158 self.provider.signing_kp.certificate_data, 159 xmlsec.constants.KeyDataFormatCertPem, 160 ) 161 ctx.key = key 162 ctx.sign(remove_xml_newlines(assertion, signature_node)) 163 164 def add_children(self, entity_descriptor: _Element): 165 self.add_idp_sso(entity_descriptor) 166 167 def add_idp_sso(self, entity_descriptor: _Element): 168 idp_sso_descriptor = SubElement( 169 entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor" 170 ) 171 idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL 172 if self.provider.verification_kp: 173 idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true" 174 175 signing_descriptor = self.get_signing_key_descriptor() 176 if signing_descriptor is not None: 177 idp_sso_descriptor.append(signing_descriptor) 178 179 for binding in self.get_slo_bindings(): 180 idp_sso_descriptor.append(binding) 181 182 for name_id_format in self.get_name_id_formats(): 183 idp_sso_descriptor.append(name_id_format) 184 185 for binding in self.get_sso_bindings(): 186 idp_sso_descriptor.append(binding) 187 188 def build_entity_descriptor(self) -> str: 189 """Build full EntityDescriptor""" 190 entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) 191 entity_descriptor.attrib["ID"] = self.xml_id 192 entity_descriptor.attrib["entityID"] = self.provider.issuer 193 194 if self.provider.signing_kp: 195 self._prepare_signature(entity_descriptor) 196 197 self.add_children(entity_descriptor) 198 199 if self.provider.signing_kp: 200 self._sign(entity_descriptor) 201 202 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 # Using type unions doesn't work with cython types (which is what lxml is) 45 def get_signing_key_descriptor(self) -> Element | None: 46 """Get Signing KeyDescriptor, if enabled for the provider""" 47 if not self.provider.signing_kp: 48 return None 49 key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") 50 key_descriptor.attrib["use"] = "signing" 51 key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo") 52 x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data") 53 x509_certificate = SubElement(x509_data, f"{{{NS_SIGNATURE}}}X509Certificate") 54 x509_certificate.text = strip_pem_header( 55 self.provider.signing_kp.certificate_data.replace("\r", "") 56 ) 57 return key_descriptor 58 59 def get_name_id_formats(self) -> Iterator[Element]: 60 """Get compatible NameID Formats""" 61 formats = [ 62 SAML_NAME_ID_FORMAT_EMAIL, 63 SAML_NAME_ID_FORMAT_PERSISTENT, 64 SAML_NAME_ID_FORMAT_X509, 65 SAML_NAME_ID_FORMAT_TRANSIENT, 66 ] 67 for name_id_format in formats: 68 element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat") 69 element.text = name_id_format 70 yield element 71 72 def get_sso_bindings(self) -> Iterator[Element]: 73 """Get all Bindings supported""" 74 binding_url_map = { 75 (SAML_BINDING_REDIRECT, "SingleSignOnService"): self.http_request.build_absolute_uri( 76 reverse( 77 "authentik_providers_saml:sso-redirect", 78 kwargs={"application_slug": self.provider.application.slug}, 79 ) 80 ), 81 (SAML_BINDING_POST, "SingleSignOnService"): self.http_request.build_absolute_uri( 82 reverse( 83 "authentik_providers_saml:sso-post", 84 kwargs={"application_slug": self.provider.application.slug}, 85 ) 86 ), 87 } 88 for binding_svc, url in binding_url_map.items(): 89 binding, svc = binding_svc 90 if self.force_binding and self.force_binding != binding: 91 continue 92 element = Element(f"{{{NS_SAML_METADATA}}}{svc}") 93 element.attrib["Binding"] = binding 94 element.attrib["Location"] = url 95 yield element 96 97 def get_slo_bindings(self) -> Iterator[Element]: 98 """Get all Bindings supported""" 99 binding_url_map = { 100 (SAML_BINDING_REDIRECT, "SingleLogoutService"): self.http_request.build_absolute_uri( 101 reverse( 102 "authentik_providers_saml:slo-redirect", 103 kwargs={"application_slug": self.provider.application.slug}, 104 ) 105 ), 106 (SAML_BINDING_POST, "SingleLogoutService"): self.http_request.build_absolute_uri( 107 reverse( 108 "authentik_providers_saml:slo-post", 109 kwargs={"application_slug": self.provider.application.slug}, 110 ) 111 ), 112 } 113 for binding_svc, url in binding_url_map.items(): 114 binding, svc = binding_svc 115 if self.force_binding and self.force_binding != binding: 116 continue 117 element = Element(f"{{{NS_SAML_METADATA}}}{svc}") 118 element.attrib["Binding"] = binding 119 element.attrib["Location"] = url 120 yield element 121 122 def _prepare_signature(self, entity_descriptor: _Element): 123 sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get( 124 self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1 125 ) 126 signature = xmlsec.template.create( 127 entity_descriptor, 128 xmlsec.constants.TransformExclC14N, 129 sign_algorithm_transform, 130 ns=xmlsec.constants.DSigNs, 131 ) 132 entity_descriptor.append(signature) 133 134 def _sign(self, entity_descriptor: _Element): 135 digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get( 136 self.provider.digest_algorithm, xmlsec.constants.TransformSha1 137 ) 138 assertion = entity_descriptor.xpath("//md:EntityDescriptor", namespaces=NS_MAP)[0] 139 xmlsec.tree.add_ids(assertion, ["ID"]) 140 signature_node = xmlsec.tree.find_node(assertion, xmlsec.constants.NodeSignature) 141 ref = xmlsec.template.add_reference( 142 signature_node, 143 digest_algorithm_transform, 144 uri="#" + self.xml_id, 145 ) 146 xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped) 147 xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N) 148 key_info = xmlsec.template.ensure_key_info(signature_node) 149 xmlsec.template.add_x509_data(key_info) 150 151 ctx = xmlsec.SignatureContext() 152 153 key = xmlsec.Key.from_memory( 154 self.provider.signing_kp.key_data, 155 xmlsec.constants.KeyDataFormatPem, 156 None, 157 ) 158 key.load_cert_from_memory( 159 self.provider.signing_kp.certificate_data, 160 xmlsec.constants.KeyDataFormatCertPem, 161 ) 162 ctx.key = key 163 ctx.sign(remove_xml_newlines(assertion, signature_node)) 164 165 def add_children(self, entity_descriptor: _Element): 166 self.add_idp_sso(entity_descriptor) 167 168 def add_idp_sso(self, entity_descriptor: _Element): 169 idp_sso_descriptor = SubElement( 170 entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor" 171 ) 172 idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL 173 if self.provider.verification_kp: 174 idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true" 175 176 signing_descriptor = self.get_signing_key_descriptor() 177 if signing_descriptor is not None: 178 idp_sso_descriptor.append(signing_descriptor) 179 180 for binding in self.get_slo_bindings(): 181 idp_sso_descriptor.append(binding) 182 183 for name_id_format in self.get_name_id_formats(): 184 idp_sso_descriptor.append(name_id_format) 185 186 for binding in self.get_sso_bindings(): 187 idp_sso_descriptor.append(binding) 188 189 def build_entity_descriptor(self) -> str: 190 """Build full EntityDescriptor""" 191 entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) 192 entity_descriptor.attrib["ID"] = self.xml_id 193 entity_descriptor.attrib["entityID"] = self.provider.issuer 194 195 if self.provider.signing_kp: 196 self._prepare_signature(entity_descriptor) 197 198 self.add_children(entity_descriptor) 199 200 if self.provider.signing_kp: 201 self._sign(entity_descriptor) 202 203 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:
45 def get_signing_key_descriptor(self) -> Element | None: 46 """Get Signing KeyDescriptor, if enabled for the provider""" 47 if not self.provider.signing_kp: 48 return None 49 key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") 50 key_descriptor.attrib["use"] = "signing" 51 key_info = SubElement(key_descriptor, f"{{{NS_SIGNATURE}}}KeyInfo") 52 x509_data = SubElement(key_info, f"{{{NS_SIGNATURE}}}X509Data") 53 x509_certificate = SubElement(x509_data, f"{{{NS_SIGNATURE}}}X509Certificate") 54 x509_certificate.text = strip_pem_header( 55 self.provider.signing_kp.certificate_data.replace("\r", "") 56 ) 57 return key_descriptor
Get Signing KeyDescriptor, if enabled for the provider
def
get_name_id_formats(self) -> Iterator[lxml.etree.Element]:
59 def get_name_id_formats(self) -> Iterator[Element]: 60 """Get compatible NameID Formats""" 61 formats = [ 62 SAML_NAME_ID_FORMAT_EMAIL, 63 SAML_NAME_ID_FORMAT_PERSISTENT, 64 SAML_NAME_ID_FORMAT_X509, 65 SAML_NAME_ID_FORMAT_TRANSIENT, 66 ] 67 for name_id_format in formats: 68 element = Element(f"{{{NS_SAML_METADATA}}}NameIDFormat") 69 element.text = name_id_format 70 yield element
Get compatible NameID Formats
def
get_sso_bindings(self) -> Iterator[lxml.etree.Element]:
72 def get_sso_bindings(self) -> Iterator[Element]: 73 """Get all Bindings supported""" 74 binding_url_map = { 75 (SAML_BINDING_REDIRECT, "SingleSignOnService"): self.http_request.build_absolute_uri( 76 reverse( 77 "authentik_providers_saml:sso-redirect", 78 kwargs={"application_slug": self.provider.application.slug}, 79 ) 80 ), 81 (SAML_BINDING_POST, "SingleSignOnService"): self.http_request.build_absolute_uri( 82 reverse( 83 "authentik_providers_saml:sso-post", 84 kwargs={"application_slug": self.provider.application.slug}, 85 ) 86 ), 87 } 88 for binding_svc, url in binding_url_map.items(): 89 binding, svc = binding_svc 90 if self.force_binding and self.force_binding != binding: 91 continue 92 element = Element(f"{{{NS_SAML_METADATA}}}{svc}") 93 element.attrib["Binding"] = binding 94 element.attrib["Location"] = url 95 yield element
Get all Bindings supported
def
get_slo_bindings(self) -> Iterator[lxml.etree.Element]:
97 def get_slo_bindings(self) -> Iterator[Element]: 98 """Get all Bindings supported""" 99 binding_url_map = { 100 (SAML_BINDING_REDIRECT, "SingleLogoutService"): self.http_request.build_absolute_uri( 101 reverse( 102 "authentik_providers_saml:slo-redirect", 103 kwargs={"application_slug": self.provider.application.slug}, 104 ) 105 ), 106 (SAML_BINDING_POST, "SingleLogoutService"): self.http_request.build_absolute_uri( 107 reverse( 108 "authentik_providers_saml:slo-post", 109 kwargs={"application_slug": self.provider.application.slug}, 110 ) 111 ), 112 } 113 for binding_svc, url in binding_url_map.items(): 114 binding, svc = binding_svc 115 if self.force_binding and self.force_binding != binding: 116 continue 117 element = Element(f"{{{NS_SAML_METADATA}}}{svc}") 118 element.attrib["Binding"] = binding 119 element.attrib["Location"] = url 120 yield element
Get all Bindings supported
def
add_idp_sso(self, entity_descriptor: lxml.etree._Element):
168 def add_idp_sso(self, entity_descriptor: _Element): 169 idp_sso_descriptor = SubElement( 170 entity_descriptor, f"{{{NS_SAML_METADATA}}}IDPSSODescriptor" 171 ) 172 idp_sso_descriptor.attrib["protocolSupportEnumeration"] = NS_SAML_PROTOCOL 173 if self.provider.verification_kp: 174 idp_sso_descriptor.attrib["WantAuthnRequestsSigned"] = "true" 175 176 signing_descriptor = self.get_signing_key_descriptor() 177 if signing_descriptor is not None: 178 idp_sso_descriptor.append(signing_descriptor) 179 180 for binding in self.get_slo_bindings(): 181 idp_sso_descriptor.append(binding) 182 183 for name_id_format in self.get_name_id_formats(): 184 idp_sso_descriptor.append(name_id_format) 185 186 for binding in self.get_sso_bindings(): 187 idp_sso_descriptor.append(binding)
def
build_entity_descriptor(self) -> str:
189 def build_entity_descriptor(self) -> str: 190 """Build full EntityDescriptor""" 191 entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) 192 entity_descriptor.attrib["ID"] = self.xml_id 193 entity_descriptor.attrib["entityID"] = self.provider.issuer 194 195 if self.provider.signing_kp: 196 self._prepare_signature(entity_descriptor) 197 198 self.add_children(entity_descriptor) 199 200 if self.provider.signing_kp: 201 self._sign(entity_descriptor) 202 203 return tostring(entity_descriptor).decode()
Build full EntityDescriptor