diff --git a/authentik/providers/radius/api/providers.py b/authentik/providers/radius/api/providers.py index 68e219dba694..67a512bc26cc 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,16 @@ def get_attributes(self, provider: RadiusProvider): RadiusProviderPropertyMapping, ["packet"], ) - dict = Dictionary("authentik/providers/radius/dictionaries/dictionary") + dict = Dictionary( + str( + settings.BASE_DIR + / "authentik" + / "providers" + / "radius" + / "dictionaries" + / "dictionary" + ) + ) packet = AuthPacket() packet.secret = provider.shared_secret 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 diff --git a/authentik/sources/saml/api/source.py b/authentik/sources/saml/api/source.py index 007079757659..5cf4dc7ea60d 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", + "encryption_kp", ] 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/migrations/0016_samlsource_encryption_kp.py b/authentik/sources/saml/migrations/0016_samlsource_encryption_kp.py new file mode 100644 index 000000000000..3f319e2c3976 --- /dev/null +++ b/authentik/sources/saml/migrations/0016_samlsource_encryption_kp.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.8 on 2024-08-07 17:33 + +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", "0015_groupsamlsourceconnection_samlsourcepropertymapping"), + ] + + 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/models.py b/authentik/sources/saml/models.py index 99c1c2e71fbc..0b67a060a6c4 100644 --- a/authentik/sources/saml/models.py +++ b/authentik/sources/saml/models.py @@ -156,6 +156,20 @@ 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"), + related_name="+", + ) digest_algorithm = models.TextField( choices=( diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py index 34bd4b5b6665..6a85022223e1 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]: # noqa: UP007 + """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" + 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.encryption_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,10 @@ 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) + 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..c09efeeee27f 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, @@ -76,11 +77,43 @@ def parse(self): self._root_xml = b64decode(raw_response.encode()) self._root = fromstring(self._root_xml) + if self._source.encryption_kp: + 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.encryption_kp.key_data, + xmlsec.constants.KeyDataFormatPem, + ) + + manager.add_key(key) + encryption_context = xmlsec.EncryptionContext(manager) + + encrypted_assertion = self._root.find(f".//{{{NS_SAML_ASSERTION}}}EncryptedAssertion") + 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.Error as exc: + raise InvalidEncryption() from exc + + 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( @@ -101,9 +134,9 @@ 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 - LOGGER.debug("Successfully verified signautre") + except xmlsec.Error as exc: + raise InvalidSignature() from exc + LOGGER.debug("Successfully verified signature") def _verify_request_id(self): if self._source.allow_idp_initiated: 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_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() 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() diff --git a/blueprints/schema.json b/blueprints/schema.json index 0a7811f3b61d..8ae6d8156f8c 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7407,6 +7407,12 @@ "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)." + }, + "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 ba413f678689..e504873ea4d7 100644 --- a/schema.yml +++ b/schema.yml @@ -46361,6 +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).' + 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 @@ -49178,6 +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).' + 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 @@ -49363,6 +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).' + 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 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 18d11dde72eb..faaebe7866ef 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -508,6 +508,19 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm + + +

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

+
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.