From 0a86ca11e350744516e4396fb85a1b4fdbd489dd Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 13 Jun 2024 09:06:51 +0200 Subject: [PATCH 01/19] source/saml: Updated backend for encrypted assertion support --- authentik/sources/saml/models.py | 8 ++++++ authentik/sources/saml/processors/metadata.py | 19 +++++++++++++ authentik/sources/saml/processors/response.py | 27 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py index 99c1c2e71fbc..8528d5943dc9 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -102,6 +102,14 @@ class SAMLSource(Source): verbose_name=_("SLO URL"), help_text=_("Optional URL if your IDP supports Single-Logout."), ) + request_encrypted_assertions = models.BooleanField( + default=False, + help_text=_( + "When enabled, the SAML IdP will encrypt the assertion element using the public " + "key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion " + "element, which will be decrypted by the private key of the service provider." + ), + ) allow_idp_initiated = models.BooleanField( default=False, diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 34bd4b5b6665..3a07d2b4d61a 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -46,6 +46,20 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return key_descriptor return None + def get_encryption_key_descriptor(self) -> Optional[Element]: + """Get Encryption KeyDescriptor, if encrypted assertion is requested""" + if self.source.request_encrypted_assertions: + key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") + key_descriptor.attrib["use"] = "encryption" + 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 = [ @@ -74,6 +88,11 @@ def build_entity_descriptor(self) -> str: if signing_descriptor is not None: sp_sso_descriptor.append(signing_descriptor) + encryption_descriptor = self.get_encryption_key_descriptor() + if encryption_descriptor is not None: + sp_sso_descriptor.append(encryption_descriptor) + sp_sso_descriptor.attrib["WantAssertionsEncrypted"] = "true" + for name_id_format in self.get_name_id_formats(): sp_sso_descriptor.append(name_id_format) diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 62d8dfd0ad20..70e428dd15bf 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -76,11 +76,38 @@ def parse(self): self._root_xml = b64decode(raw_response.encode()) self._root = fromstring(self._root_xml) + if self._source.request_encrypted_assertions == True: + self._decrypt_response() + if self._source.verification_kp: self._verify_signed() self._verify_request_id() self._verify_status() + def _decrypt_response(self): + """Decrypt SAMLResponse EncryptedAssertion Element""" + + manager = xmlsec.KeysManager() + key = xmlsec.Key.from_memory( + self._source.signing_kp.key_data, + xmlsec.constants.KeyDataFormatPem, + ) + + manager.add_key(key) + encryption_context = xmlsec.EncryptionContext(manager) + + encrypted_assertion = self._root.find(".//{urn:oasis:names:tc:SAML:2.0:assertion}EncryptedAssertion") + encrypted_data = xmlsec.tree.find_child(encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs) + decrypted_assertion = encryption_context.decrypt(encrypted_data) + + + index_of = self._root.index(encrypted_assertion) + self._root.remove(encrypted_assertion) + self._root.insert( + index_of, + decrypted_assertion, + ) + def _verify_signed(self): """Verify SAML Response's Signature""" signature_nodes = self._root.xpath( From 35cd3eda45a9b3ae2f03679a4abc7bf6ec72bd41 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 13 Jun 2024 09:49:09 +0200 Subject: [PATCH 02/19] source/saml: all lint-fix checks passed --- authentik/sources/saml/models.py | 6 +++--- authentik/sources/saml/processors/metadata.py | 2 +- authentik/sources/saml/processors/response.py | 15 +++++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py index 8528d5943dc9..ef1def6bbafc 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -105,9 +105,9 @@ class SAMLSource(Source): request_encrypted_assertions = models.BooleanField( default=False, help_text=_( - "When enabled, the SAML IdP will encrypt the assertion element using the public " - "key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion " - "element, which will be decrypted by the private key of the service provider." + "When enabled, the SAML IdP will encrypt the assertion element using the public " + "key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion " + "element, which will be decrypted by the private key of the service provider." ), ) diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 3a07d2b4d61a..ef34894f77eb 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -46,7 +46,7 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return key_descriptor return None - def get_encryption_key_descriptor(self) -> Optional[Element]: + def get_encryption_key_descriptor(self) -> Element | None: """Get Encryption KeyDescriptor, if encrypted assertion is requested""" if self.source.request_encrypted_assertions: key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 70e428dd15bf..b8d4661ed914 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -76,7 +76,7 @@ def parse(self): self._root_xml = b64decode(raw_response.encode()) self._root = fromstring(self._root_xml) - if self._source.request_encrypted_assertions == True: + if self._source.request_encrypted_assertions: self._decrypt_response() if self._source.verification_kp: @@ -96,16 +96,19 @@ def _decrypt_response(self): manager.add_key(key) encryption_context = xmlsec.EncryptionContext(manager) - encrypted_assertion = self._root.find(".//{urn:oasis:names:tc:SAML:2.0:assertion}EncryptedAssertion") - encrypted_data = xmlsec.tree.find_child(encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs) + encrypted_assertion = self._root.find( + ".//{urn:oasis:names:tc:SAML:2.0:assertion}EncryptedAssertion" + ) + encrypted_data = xmlsec.tree.find_child( + encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs + ) decrypted_assertion = encryption_context.decrypt(encrypted_data) - index_of = self._root.index(encrypted_assertion) self._root.remove(encrypted_assertion) self._root.insert( - index_of, - decrypted_assertion, + index_of, + decrypted_assertion, ) def _verify_signed(self): From a587ae5f3895dc8f3a47abb93a5fbe0b9e026626 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 13 Jun 2024 11:05:09 +0200 Subject: [PATCH 03/19] source/saml: Used Optional type instead of union, on enc_key_descriptor type hint --- authentik/sources/saml/processors/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index ef34894f77eb..3a07d2b4d61a 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -46,7 +46,7 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return key_descriptor return None - def get_encryption_key_descriptor(self) -> Element | None: + def get_encryption_key_descriptor(self) -> Optional[Element]: """Get Encryption KeyDescriptor, if encrypted assertion is requested""" if self.source.request_encrypted_assertions: key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") From 4bd9040b186f247b5ebdfc77c022391521fcd7f2 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 13 Jun 2024 11:05:43 +0200 Subject: [PATCH 04/19] source/saml: request_encrypted_assertion model field migration --- ...samlsource_request_encrypted_assertions.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py diff --git a/authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py b/authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py new file mode 100644 index 000000000000..f6824a939b5a --- /dev/null +++ b/authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.6 on 2024-06-13 08:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_sources_saml", "0014_alter_samlsource_digest_algorithm_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="samlsource", + name="request_encrypted_assertions", + field=models.BooleanField( + default=False, + help_text="When enabled, the SAML IdP will encrypt the assertion element using the public key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion element, which will be decrypted by the private key of the service provider.", + ), + ), + ] From 2addaefdef54f33faab9825c2eb0c5e77d78c52b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 4 Jul 2024 08:59:14 +0200 Subject: [PATCH 05/19] source/saml: Added 'noqa' comment to type hint on encryption key descriptor --- authentik/sources/saml/processors/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 3a07d2b4d61a..c12a55556e21 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -46,7 +46,7 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return key_descriptor return None - def get_encryption_key_descriptor(self) -> Optional[Element]: + def get_encryption_key_descriptor(self) -> Optional[Element]: # noqa: UP007 """Get Encryption KeyDescriptor, if encrypted assertion is requested""" if self.source.request_encrypted_assertions: key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") From 2425002eb00c6c94636902f67fc2fcc6fa01f49e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 6 Jul 2024 19:41:37 +0200 Subject: [PATCH 06/19] small fix Signed-off-by: Jens Langhammer --- authentik/sources/saml/processors/metadata.py | 2 +- authentik/sources/saml/processors/response.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index c12a55556e21..0820a31595c9 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -46,7 +46,7 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return key_descriptor return None - def get_encryption_key_descriptor(self) -> Optional[Element]: # noqa: UP007 + def get_encryption_key_descriptor(self) -> Optional[Element]: # noqa: UP007 """Get Encryption KeyDescriptor, if encrypted assertion is requested""" if self.source.request_encrypted_assertions: key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index b8d4661ed914..bb891ad3d569 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -86,7 +86,6 @@ def parse(self): def _decrypt_response(self): """Decrypt SAMLResponse EncryptedAssertion Element""" - manager = xmlsec.KeysManager() key = xmlsec.Key.from_memory( self._source.signing_kp.key_data, @@ -96,9 +95,7 @@ def _decrypt_response(self): manager.add_key(key) encryption_context = xmlsec.EncryptionContext(manager) - encrypted_assertion = self._root.find( - ".//{urn:oasis:names:tc:SAML:2.0:assertion}EncryptedAssertion" - ) + encrypted_assertion = self._root.find(f".//{{{NS_SAML_ASSERTION}}}EncryptedAssertion") encrypted_data = xmlsec.tree.find_child( encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs ) From 60c21d7865bd405e3e86536b57a8feb703afe6ad Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 6 Jul 2024 19:45:21 +0200 Subject: [PATCH 07/19] add to UI Signed-off-by: Jens Langhammer --- authentik/sources/saml/api/source.py | 1 + blueprints/schema.json | 5 +++++ schema.yml | 18 ++++++++++++++++++ web/src/admin/sources/saml/SAMLSourceForm.ts | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/authentik/sources/saml/api/source.py b/authentik/sources/saml/api/source.py index 007079757659..2e87077dc681 100644 --- a/authentik/sources/saml/api/source.py +++ b/authentik/sources/saml/api/source.py @@ -33,6 +33,7 @@ class Meta: "digest_algorithm", "signature_algorithm", "temporary_user_delete_after", + "request_encrypted_assertions", ] diff --git a/blueprints/schema.json b/blueprints/schema.json index 0a7811f3b61d..b1d777df213e 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7407,6 +7407,11 @@ "minLength": 1, "title": "Delete temporary users after", "description": "Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. (Format: hours=1;minutes=2;seconds=3)." + }, + "request_encrypted_assertions": { + "type": "boolean", + "title": "Request encrypted assertions", + "description": "When enabled, the SAML IdP will encrypt the assertion element using the public key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion element, which will be decrypted by the private key of the service provider." } }, "required": [] diff --git a/schema.yml b/schema.yml index ba413f678689..403009c4d12a 100644 --- a/schema.yml +++ b/schema.yml @@ -46361,6 +46361,12 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' + request_encrypted_assertions: + type: boolean + description: When enabled, the SAML IdP will encrypt the assertion element + using the public key of the SP signing keypair. The SAMLResponse will + contain an EncryptedAssertion element, which will be decrypted by the + private key of the service provider. PatchedSCIMMappingRequest: type: object description: SCIMMapping Serializer @@ -49178,6 +49184,12 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' + request_encrypted_assertions: + type: boolean + description: When enabled, the SAML IdP will encrypt the assertion element + using the public key of the SP signing keypair. The SAMLResponse will + contain an EncryptedAssertion element, which will be decrypted by the + private key of the service provider. required: - component - icon @@ -49363,6 +49375,12 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' + request_encrypted_assertions: + type: boolean + description: When enabled, the SAML IdP will encrypt the assertion element + using the public key of the SP signing keypair. The SAMLResponse will + contain an EncryptedAssertion element, which will be decrypted by the + private key of the service provider. required: - name - pre_authentication_flow diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts index 18d11dde72eb..ac35ef7ca3d4 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -508,6 +508,25 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm + + + From 09c35b142d4445a653182daed864747af54f05a7 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 9 Jul 2024 11:35:01 +0200 Subject: [PATCH 08/19] add some error handling Signed-off-by: Jens Langhammer --- authentik/sources/saml/exceptions.py | 4 ++++ authentik/sources/saml/processors/response.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/authentik/sources/saml/exceptions.py b/authentik/sources/saml/exceptions.py index 057a040aa2f7..45534e07f904 100644 --- a/authentik/sources/saml/exceptions.py +++ b/authentik/sources/saml/exceptions.py @@ -19,5 +19,9 @@ class MismatchedRequestID(SAMLException): """Exception raised when the returned request ID doesn't match the saved ID.""" +class InvalidEncryption(SAMLException): + """Encryption of XML Object is either missing or invalid""" + + class InvalidSignature(SAMLException): """Signature of XML Object is either missing or invalid""" diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index bb891ad3d569..46afe8780093 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -23,6 +23,7 @@ from authentik.core.sources.flow_manager import SourceFlowManager from authentik.lib.utils.time import timedelta_from_string from authentik.sources.saml.exceptions import ( + InvalidEncryption, InvalidSignature, MismatchedRequestID, MissingSAMLResponse, @@ -96,10 +97,15 @@ def _decrypt_response(self): encryption_context = xmlsec.EncryptionContext(manager) encrypted_assertion = self._root.find(f".//{{{NS_SAML_ASSERTION}}}EncryptedAssertion") + if not encrypted_assertion: + raise InvalidEncryption() encrypted_data = xmlsec.tree.find_child( encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs ) - decrypted_assertion = encryption_context.decrypt(encrypted_data) + try: + decrypted_assertion = encryption_context.decrypt(encrypted_data) + except (xmlsec.InternalError, xmlsec.VerificationError) as exc: + raise InvalidEncryption() from exc index_of = self._root.index(encrypted_assertion) self._root.remove(encrypted_assertion) @@ -130,7 +136,7 @@ def _verify_signed(self): ctx.verify(signature_node) except (xmlsec.InternalError, xmlsec.VerificationError) as exc: raise InvalidSignature from exc - LOGGER.debug("Successfully verified signautre") + LOGGER.debug("Successfully verified signature") def _verify_request_id(self): if self._source.allow_idp_initiated: From 3f2fca1001e8d039442454bc8a1b339f1c7c0694 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 29 Jul 2024 12:17:45 +0200 Subject: [PATCH 09/19] sources/saml: Pivot to encryption_kp model field, instead of request_encryption bool --- authentik/sources/saml/models.py | 20 +++++++++++-------- authentik/sources/saml/processors/metadata.py | 6 +++--- authentik/sources/saml/processors/response.py | 4 ++-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py index ef1def6bbafc..531990c1e2e0 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -102,14 +102,6 @@ class SAMLSource(Source): verbose_name=_("SLO URL"), help_text=_("Optional URL if your IDP supports Single-Logout."), ) - request_encrypted_assertions = models.BooleanField( - default=False, - help_text=_( - "When enabled, the SAML IdP will encrypt the assertion element using the public " - "key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion " - "element, which will be decrypted by the private key of the service provider." - ), - ) allow_idp_initiated = models.BooleanField( default=False, @@ -164,6 +156,18 @@ class SAMLSource(Source): on_delete=models.SET_NULL, verbose_name=_("Signing Keypair"), ) + encryption_kp = models.ForeignKey( + CertificateKeyPair, + default=None, + null=True, + blank=True, + help_text=_("When selected, incoming assertions are encrypted by the IdP using the public " + "key of the encryption keypair. The assertion is decrypted by the SP using the " + "the private key." + ), + on_delete=models.SET_NULL, + verbose_name=_("Encryption Keypair"), + ) digest_algorithm = models.TextField( choices=( diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 0820a31595c9..540a9f78ff68 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -47,15 +47,15 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return None def get_encryption_key_descriptor(self) -> Optional[Element]: # noqa: UP007 - """Get Encryption KeyDescriptor, if encrypted assertion is requested""" - if self.source.request_encrypted_assertions: + """Get Encryption KeyDescriptor, if enabled for the source is requested""" + if self.source.encryption_kp: key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") key_descriptor.attrib["use"] = "encryption" 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", "") + self.source.encryption_kp.certificate_data.replace("\r", "") ).replace("\n", "") return key_descriptor return None diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 46afe8780093..89e7ebb1a90c 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -77,7 +77,7 @@ def parse(self): self._root_xml = b64decode(raw_response.encode()) self._root = fromstring(self._root_xml) - if self._source.request_encrypted_assertions: + if self._source.encryption_kp: self._decrypt_response() if self._source.verification_kp: @@ -89,7 +89,7 @@ def _decrypt_response(self): """Decrypt SAMLResponse EncryptedAssertion Element""" manager = xmlsec.KeysManager() key = xmlsec.Key.from_memory( - self._source.signing_kp.key_data, + self._source.encryption_kp.key_data, xmlsec.constants.KeyDataFormatPem, ) From e2973db0299408afd6ef9a216b235148e77804fe Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 29 Jul 2024 12:19:29 +0200 Subject: [PATCH 10/19] sources/saml: Typo fix --- authentik/sources/saml/processors/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 540a9f78ff68..1dc28884e6e3 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -47,7 +47,7 @@ def get_signing_key_descriptor(self) -> Optional[Element]: # noqa: UP007 return None def get_encryption_key_descriptor(self) -> Optional[Element]: # noqa: UP007 - """Get Encryption KeyDescriptor, if enabled for the source is requested""" + """Get Encryption KeyDescriptor, if enabled for the source""" if self.source.encryption_kp: key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor") key_descriptor.attrib["use"] = "encryption" From 9f93bbb5ccb67960bc741432579e3c174f6c5a72 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 14:52:59 +0200 Subject: [PATCH 11/19] re-create migrations Signed-off-by: Jens Langhammer --- authentik/sources/saml/api/source.py | 2 +- .../0015_samlsource_encryption_kp.py | 29 +++++++++++++ ...samlsource_request_encrypted_assertions.py | 21 ---------- authentik/sources/saml/models.py | 8 ++-- blueprints/schema.json | 9 ++-- schema.yml | 42 +++++++++++-------- 6 files changed, 64 insertions(+), 47 deletions(-) create mode 100644 authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py delete mode 100644 authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py diff --git a/authentik/sources/saml/api/source.py b/authentik/sources/saml/api/source.py index 2e87077dc681..5cf4dc7ea60d 100644 --- a/authentik/sources/saml/api/source.py +++ b/authentik/sources/saml/api/source.py @@ -33,7 +33,7 @@ class Meta: "digest_algorithm", "signature_algorithm", "temporary_user_delete_after", - "request_encrypted_assertions", + "encryption_kp", ] diff --git a/authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py b/authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py new file mode 100644 index 000000000000..3d6b4b699681 --- /dev/null +++ b/authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.7 on 2024-08-07 12:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_crypto", "0004_alter_certificatekeypair_name"), + ("authentik_sources_saml", "0014_alter_samlsource_digest_algorithm_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="samlsource", + name="encryption_kp", + field=models.ForeignKey( + blank=True, + default=None, + help_text="When selected, incoming assertions are encrypted by the IdP using the public key of the encryption keypair. The assertion is decrypted by the SP using the the private key.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="authentik_crypto.certificatekeypair", + verbose_name="Encryption Keypair", + ), + ), + ] diff --git a/authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py b/authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py deleted file mode 100644 index f6824a939b5a..000000000000 --- a/authentik/sources/saml/migrations/0015_samlsource_request_encrypted_assertions.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-13 08:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("authentik_sources_saml", "0014_alter_samlsource_digest_algorithm_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="samlsource", - name="request_encrypted_assertions", - field=models.BooleanField( - default=False, - help_text="When enabled, the SAML IdP will encrypt the assertion element using the public key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion element, which will be decrypted by the private key of the service provider.", - ), - ), - ] diff --git a/authentik/sources/saml/models.py b/authentik/sources/saml/models.py index 531990c1e2e0..0b67a060a6c4 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -161,12 +161,14 @@ class SAMLSource(Source): default=None, null=True, blank=True, - help_text=_("When selected, incoming assertions are encrypted by the IdP using the public " - "key of the encryption keypair. The assertion is decrypted by the SP using the " - "the private key." + help_text=_( + "When selected, incoming assertions are encrypted by the IdP using the public " + "key of the encryption keypair. The assertion is decrypted by the SP using the " + "the private key." ), on_delete=models.SET_NULL, verbose_name=_("Encryption Keypair"), + related_name="+", ) digest_algorithm = models.TextField( diff --git a/blueprints/schema.json b/blueprints/schema.json index b1d777df213e..8ae6d8156f8c 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7408,10 +7408,11 @@ "title": "Delete temporary users after", "description": "Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. (Format: hours=1;minutes=2;seconds=3)." }, - "request_encrypted_assertions": { - "type": "boolean", - "title": "Request encrypted assertions", - "description": "When enabled, the SAML IdP will encrypt the assertion element using the public key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion element, which will be decrypted by the private key of the service provider." + "encryption_kp": { + "type": "string", + "format": "uuid", + "title": "Encryption Keypair", + "description": "When selected, incoming assertions are encrypted by the IdP using the public key of the encryption keypair. The assertion is decrypted by the SP using the the private key." } }, "required": [] diff --git a/schema.yml b/schema.yml index 403009c4d12a..e504873ea4d7 100644 --- a/schema.yml +++ b/schema.yml @@ -46361,12 +46361,14 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' - request_encrypted_assertions: - type: boolean - description: When enabled, the SAML IdP will encrypt the assertion element - using the public key of the SP signing keypair. The SAMLResponse will - contain an EncryptedAssertion element, which will be decrypted by the - private key of the service provider. + encryption_kp: + type: string + format: uuid + nullable: true + title: Encryption Keypair + description: When selected, incoming assertions are encrypted by the IdP + using the public key of the encryption keypair. The assertion is decrypted + by the SP using the the private key. PatchedSCIMMappingRequest: type: object description: SCIMMapping Serializer @@ -49184,12 +49186,14 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' - request_encrypted_assertions: - type: boolean - description: When enabled, the SAML IdP will encrypt the assertion element - using the public key of the SP signing keypair. The SAMLResponse will - contain an EncryptedAssertion element, which will be decrypted by the - private key of the service provider. + encryption_kp: + type: string + format: uuid + nullable: true + title: Encryption Keypair + description: When selected, incoming assertions are encrypted by the IdP + using the public key of the encryption keypair. The assertion is decrypted + by the SP using the the private key. required: - component - icon @@ -49375,12 +49379,14 @@ components: description: 'Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format ''transient'', and the user doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).' - request_encrypted_assertions: - type: boolean - description: When enabled, the SAML IdP will encrypt the assertion element - using the public key of the SP signing keypair. The SAMLResponse will - contain an EncryptedAssertion element, which will be decrypted by the - private key of the service provider. + encryption_kp: + type: string + format: uuid + nullable: true + title: Encryption Keypair + description: When selected, incoming assertions are encrypted by the IdP + using the public key of the encryption keypair. The assertion is decrypted + by the SP using the the private key. required: - name - pre_authentication_flow From bb7e5e70b6b6b84fc95fcdf9cdde230d0f241bb2 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 14:57:35 +0200 Subject: [PATCH 12/19] update web Signed-off-by: Jens Langhammer --- .../common/ak-crypto-certificate-search.ts | 5 ++-- web/src/admin/sources/saml/SAMLSourceForm.ts | 30 ++++++++----------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/web/src/admin/common/ak-crypto-certificate-search.ts b/web/src/admin/common/ak-crypto-certificate-search.ts index 186481a7a562..c2227168219c 100644 --- a/web/src/admin/common/ak-crypto-certificate-search.ts +++ b/web/src/admin/common/ak-crypto-certificate-search.ts @@ -41,9 +41,8 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) name: string | null | undefined; /** - * Set to `true` if you want to find pairs that don't have a valid key. Of our 14 searches, 11 - * require the key, 3 do not (as of 2023-08-01). - * + * Set to `true` to allow certificates without private key to show up. When set to `false`, + * a private key is not required to be set. * @attr */ @property({ type: Boolean, attribute: "nokey" }) diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts index ac35ef7ca3d4..faaebe7866ef 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -508,24 +508,18 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm - - + + +

+ ${msg( + "When selected, encrypted assertions will be decrypted using this keypair.", + )} +

From efed2116f1d345a47a3e58cefafc66803fd1836b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 15:03:15 +0200 Subject: [PATCH 13/19] add to release notes Signed-off-by: Jens Langhammer --- website/docs/releases/2024/v2024.8.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/releases/2024/v2024.8.md b/website/docs/releases/2024/v2024.8.md index 6634f79dd2d1..fd9b0661c523 100644 --- a/website/docs/releases/2024/v2024.8.md +++ b/website/docs/releases/2024/v2024.8.md @@ -50,6 +50,10 @@ To try out the release candidate, replace your Docker image tag with the latest ## New features +- **SAML Source encryption support** + + It is now possible to configure a SAML Source to decrypt and validate encrypted assertions. This can be configured by certaing a [Certificate-keypair](../../core/certificates.md) and selecting it in the SAML Source. + ## Upgrading This release does not introduce any new requirements. From 2ed6ff121aa8c980fa20be2661765da67eea9a64 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 17:56:56 +0200 Subject: [PATCH 14/19] unrelated fix Signed-off-by: Jens Langhammer --- authentik/providers/saml/processors/assertion.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/authentik/providers/saml/processors/assertion.py b/authentik/providers/saml/processors/assertion.py index 8c18f10b90e9..845a7b9395a0 100644 --- a/authentik/providers/saml/processors/assertion.py +++ b/authentik/providers/saml/processors/assertion.py @@ -18,7 +18,7 @@ from authentik.providers.saml.utils import get_random_id from authentik.providers.saml.utils.time import get_time_string from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME -from authentik.sources.saml.exceptions import UnsupportedNameIDFormat +from authentik.sources.saml.exceptions import InvalidSignature, UnsupportedNameIDFormat from authentik.sources.saml.processors.constants import ( DIGEST_ALGORITHM_TRANSLATION_MAP, NS_MAP, @@ -318,6 +318,9 @@ def build_response(self) -> str: xmlsec.constants.KeyDataFormatCertPem, ) ctx.key = key - ctx.sign(signature_node) + try: + ctx.sign(signature_node) + except xmlsec.Error as exc: + raise InvalidSignature() from exc return etree.tostring(root_response).decode("utf-8") # nosec From dc6f2dcb5184da926d39980370a86c2c5f38b145 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 17:57:13 +0200 Subject: [PATCH 15/19] add improve error handling, add tests Signed-off-by: Jens Langhammer --- authentik/sources/saml/processors/response.py | 8 +-- .../saml/tests/fixtures/encrypted-key.pem | 51 +++++++++++++++++++ .../tests/fixtures/response_encrypted.xml | 42 +++++++++++++++ authentik/sources/saml/tests/test_response.py | 49 +++++++++++++++++- 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 authentik/sources/saml/tests/fixtures/encrypted-key.pem create mode 100644 authentik/sources/saml/tests/fixtures/response_encrypted.xml diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 89e7ebb1a90c..c09efeeee27f 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -97,14 +97,14 @@ def _decrypt_response(self): encryption_context = xmlsec.EncryptionContext(manager) encrypted_assertion = self._root.find(f".//{{{NS_SAML_ASSERTION}}}EncryptedAssertion") - if not encrypted_assertion: + if encrypted_assertion is None: raise InvalidEncryption() encrypted_data = xmlsec.tree.find_child( encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs ) try: decrypted_assertion = encryption_context.decrypt(encrypted_data) - except (xmlsec.InternalError, xmlsec.VerificationError) as exc: + except xmlsec.Error as exc: raise InvalidEncryption() from exc index_of = self._root.index(encrypted_assertion) @@ -134,8 +134,8 @@ def _verify_signed(self): ctx.set_enabled_key_data([xmlsec.constants.KeyDataX509]) try: ctx.verify(signature_node) - except (xmlsec.InternalError, xmlsec.VerificationError) as exc: - raise InvalidSignature from exc + except xmlsec.Error as exc: + raise InvalidSignature() from exc LOGGER.debug("Successfully verified signature") def _verify_request_id(self): diff --git a/authentik/sources/saml/tests/fixtures/encrypted-key.pem b/authentik/sources/saml/tests/fixtures/encrypted-key.pem new file mode 100644 index 000000000000..bce94cba7482 --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/encrypted-key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/authentik/sources/saml/tests/fixtures/response_encrypted.xml b/authentik/sources/saml/tests/fixtures/response_encrypted.xml new file mode 100644 index 000000000000..50c881ac8dd5 --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/response_encrypted.xml @@ -0,0 +1,42 @@ + + http://localhost:9321/realms/master + + + + + + + + + + + Os1F6dK4wUwz3tzVtcTXHZID9S4qbkIPnlDX8MAqShA= + + + Af1vWp2FNIbwhI8+VMtvY0VuT7fy7rj6NSyzdV89hzPaKRWy5V8F1XSfFHOG9SPVOldB4azgPPSo5I2AocPoy9EepY2wrV8CtRA+W4W+4BKg2jk/iiGsPoXE0HVxstUPrl4t6wcwFqKEYcqT9Xunpa/3WHWguja9ariywuOmVostQgPnbq3WpmdIzD/faMgbJ1bFVyS8xdxUbEhDKd17+Io+eyvc0UMlkkESBNw9jSsUg4Fa3uFr5VYSKWW2ssBXOjjqLAg013lZZUoCbNDXmUe6UXcpySVlrvkVlWHptPPs3lSOO1io7vrywh9hjV498gFvverYcZDyE44Pvb9kdQ== + + + MIICmzCCAYMCBgGQlr/VwDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzA5MDkwNjEyWhcNMzQwNzA5MDkwNzUyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWdkifjNef+IeyFpWIRId7eLU1WQpVXMdbdfADgPBtzjr87ekqWLFLswfzb7zfKFcKLKM4pALPyjIjkSZeJ0ctpg4OEBtio55UBeMiqPm5EgwpuqhlWATQzH3yexwlFOVCbj44ZkhPqmnAI1d3iyFK0OjV35ar7C2Eu54+qGH/VeDyxQ+19WEq0rOxmCqoK27JJU4rPR+42SN0CxoSVDpZSfbNu4iQ8lW0zQ7GOSDarTWjbJ7yd9ULqhBRN8DOIG+GypeyZbQfuLmZPmPWQZdTNovS+6se9zTs4RgVCtjtU2qfCwh76Fw0kR7ignfd2PhrH30G7tybPwLQ2WobTUmRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHs19PueEZBZ54Uxq8c2IgVErn1RQDKR/GO5AuQfZZqODlGM0ypP9eAXt8SPl98SzVlynKek1z6RsH+1GhyOemxAkmJmI0ema5HwUSarrOtrASmHYNV2QgTcsyRPmv6KYZNWcLnmYDA2ZcGmvKruLe74QaW6gW5/i4SvaAcSVlyVI9w6e/AGfglGMjScGD68adiPJbudswRfG9gPEHRe9nQKtxBw98g68VqUD6P3KHNAYejBAfoPD5gP4bWo8RuAomc8me2bjozx9EPO8oZV6x4OYTsfavuvfgEAPenNF11hGjR0P3PeZRwICJMErrl1R+BxSnetA26vHZ8ypLSKTug= + + + + + + + + + + + + + + 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 + + + + diff --git a/authentik/sources/saml/tests/test_response.py b/authentik/sources/saml/tests/test_response.py index a56e3d4c1980..2d85154202ad 100644 --- a/authentik/sources/saml/tests/test_response.py +++ b/authentik/sources/saml/tests/test_response.py @@ -5,9 +5,11 @@ from django.contrib.sessions.middleware import SessionMiddleware from django.test import RequestFactory, TestCase -from authentik.core.tests.utils import create_test_flow +from authentik.core.tests.utils import create_test_cert, create_test_flow +from authentik.crypto.models import CertificateKeyPair from authentik.lib.generators import generate_id from authentik.lib.tests.utils import dummy_get_response, load_fixture +from authentik.sources.saml.exceptions import InvalidEncryption from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.processors.response import ResponseProcessor @@ -77,3 +79,48 @@ def test_success(self): "path": self.source.get_user_path(), }, ) + + def test_encrypted_correct(self): + """Test encrypted""" + key = load_fixture("fixtures/encrypted-key.pem") + kp = CertificateKeyPair.objects.create( + name=generate_id(), + key_data=key, + ) + self.source.encryption_kp = kp + request = self.factory.post( + "/", + data={ + "SAMLResponse": b64encode( + load_fixture("fixtures/response_encrypted.xml").encode() + ).decode() + }, + ) + + middleware = SessionMiddleware(dummy_get_response) + middleware.process_request(request) + request.session.save() + + parser = ResponseProcessor(self.source, request) + parser.parse() + + def test_encrypted_incorrect_key(self): + """Test encrypted""" + kp = create_test_cert() + self.source.encryption_kp = kp + request = self.factory.post( + "/", + data={ + "SAMLResponse": b64encode( + load_fixture("fixtures/response_encrypted.xml").encode() + ).decode() + }, + ) + + middleware = SessionMiddleware(dummy_get_response) + middleware.process_request(request) + request.session.save() + + parser = ResponseProcessor(self.source, request) + with self.assertRaises(InvalidEncryption): + parser.parse() From c694f5b4df39be5bbb2e79dac9587489bb458023 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 19:10:40 +0200 Subject: [PATCH 16/19] test metadata with encryption and remove WantAssertionsEncrypted since it's not in the schema Signed-off-by: Jens Langhammer --- authentik/sources/saml/processors/metadata.py | 1 - authentik/sources/saml/tests/test_metadata.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 1dc28884e6e3..6a85022223e1 100644 --- a/authentik/sources/saml/processors/metadata.py +++ b/authentik/sources/saml/processors/metadata.py @@ -91,7 +91,6 @@ def build_entity_descriptor(self) -> str: encryption_descriptor = self.get_encryption_key_descriptor() if encryption_descriptor is not None: sp_sso_descriptor.append(encryption_descriptor) - sp_sso_descriptor.attrib["WantAssertionsEncrypted"] = "true" for name_id_format in self.get_name_id_formats(): sp_sso_descriptor.append(name_id_format) diff --git a/authentik/sources/saml/tests/test_metadata.py b/authentik/sources/saml/tests/test_metadata.py index 953745c2dc38..64bc7147452d 100644 --- a/authentik/sources/saml/tests/test_metadata.py +++ b/authentik/sources/saml/tests/test_metadata.py @@ -20,6 +20,7 @@ def setUp(self): slug=generate_id(), issuer="authentik", signing_kp=create_test_cert(), + encryption_kp=create_test_cert(), pre_authentication_flow=create_test_flow(), ) @@ -46,7 +47,7 @@ def test_metadata(self): metadata = ElementTree.fromstring(xml) self.assertEqual(metadata.attrib["entityID"], "authentik") - def test_metadata_without_signautre(self): + def test_metadata_without_signature(self): """Test Metadata generation being valid""" self.source.signing_kp = None self.source.save() From 7dc7972b1bcd9ffd862ed9af3ca5f244da8eebeb Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 19:10:48 +0200 Subject: [PATCH 17/19] unrelated fix to radius path Signed-off-by: Jens Langhammer --- authentik/providers/radius/api/providers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/authentik/providers/radius/api/providers.py b/authentik/providers/radius/api/providers.py index 68e219dba694..f8d19a5896cd 100644 --- a/authentik/providers/radius/api/providers.py +++ b/authentik/providers/radius/api/providers.py @@ -2,6 +2,7 @@ from base64 import b64encode +from django.conf import settings from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema @@ -100,7 +101,9 @@ def get_attributes(self, provider: RadiusProvider): RadiusProviderPropertyMapping, ["packet"], ) - dict = Dictionary("authentik/providers/radius/dictionaries/dictionary") + dict = Dictionary( + settings.BASE_DIR / "authentik" / "providers" / "radius" / "dictionaries" / "dictionary" + ) packet = AuthPacket() packet.secret = provider.shared_secret From 00a195c77095e05007599b06c3583d674eed2bd6 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 19:23:50 +0200 Subject: [PATCH 18/19] fix unrelated fix...sigh Signed-off-by: Jens Langhammer --- authentik/providers/radius/api/providers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/authentik/providers/radius/api/providers.py b/authentik/providers/radius/api/providers.py index f8d19a5896cd..67a512bc26cc 100644 --- a/authentik/providers/radius/api/providers.py +++ b/authentik/providers/radius/api/providers.py @@ -102,7 +102,14 @@ def get_attributes(self, provider: RadiusProvider): ["packet"], ) dict = Dictionary( - settings.BASE_DIR / "authentik" / "providers" / "radius" / "dictionaries" / "dictionary" + str( + settings.BASE_DIR + / "authentik" + / "providers" + / "radius" + / "dictionaries" + / "dictionary" + ) ) packet = AuthPacket() From c438557a915a4d9f400314a88691596fe0a0c116 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 7 Aug 2024 19:33:46 +0200 Subject: [PATCH 19/19] re-migrate Signed-off-by: Jens Langhammer --- ...urce_encryption_kp.py => 0016_samlsource_encryption_kp.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename authentik/sources/saml/migrations/{0015_samlsource_encryption_kp.py => 0016_samlsource_encryption_kp.py} (86%) diff --git a/authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py b/authentik/sources/saml/migrations/0016_samlsource_encryption_kp.py similarity index 86% rename from authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py rename to authentik/sources/saml/migrations/0016_samlsource_encryption_kp.py index 3d6b4b699681..3f319e2c3976 100644 --- a/authentik/sources/saml/migrations/0015_samlsource_encryption_kp.py +++ b/authentik/sources/saml/migrations/0016_samlsource_encryption_kp.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.7 on 2024-08-07 12:52 +# Generated by Django 5.0.8 on 2024-08-07 17:33 import django.db.models.deletion from django.db import migrations, models @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ("authentik_crypto", "0004_alter_certificatekeypair_name"), - ("authentik_sources_saml", "0014_alter_samlsource_digest_algorithm_and_more"), + ("authentik_sources_saml", "0015_groupsamlsourceconnection_samlsourcepropertymapping"), ] operations = [