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)
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()
http_request: django.http.request.HttpRequest
force_binding: str | None
xml_id
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_children(self, entity_descriptor: lxml.etree._Element):
165    def add_children(self, entity_descriptor: _Element):
166        self.add_idp_sso(entity_descriptor)
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