From 95644dc9615f428191d9fda0847c1b91a0b094a5 Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Sun, 31 May 2026 16:16:46 +0300 Subject: [PATCH] feat: configure SP certificate and private key via SAMLConfiguration (#1531) --- cms/version.py | 2 +- docs/admins_docs.md | 2 ++ saml_auth/admin.py | 2 +- saml_auth/custom/utils.py | 14 ++++------- saml_auth/custom/views.py | 4 +++- ...0002_samlconfiguration_sp_cert_and_more.py | 23 +++++++++++++++++++ saml_auth/models.py | 11 +++++++++ 7 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 saml_auth/migrations/0002_samlconfiguration_sp_cert_and_more.py diff --git a/cms/version.py b/cms/version.py index 08fb6546..c0a494d8 100644 --- a/cms/version.py +++ b/cms/version.py @@ -1 +1 @@ -VERSION = "8.1.2" +VERSION = "8.2.0" diff --git a/docs/admins_docs.md b/docs/admins_docs.md index 8709c60a..aec9bc7b 100644 --- a/docs/admins_docs.md +++ b/docs/admins_docs.md @@ -947,6 +947,8 @@ Select the SAML Configurations tab, create a new one and set: 3. **SSO URL**: 4. **SLO URL**: 5. **SP Metadata URL**: The metadata URL that the IDP will utilize. This can be https://{portal}/saml/metadata and is autogenerated by MediaCMS +6. **SP Certificate** (optional): SP x509 certificate (PEM). Enables encrypted/signed SAML communication. If set, the SP Private Key must also be provided, and the certificate is published in the SP metadata so the IDP can encrypt assertions to MediaCMS. +7. **SP Private Key** (optional): SP private key (PEM). Used to sign AuthnRequests/LogoutRequests and to decrypt assertions encrypted by the IDP. Required if SP Certificate is provided. - Step 3: Set other Options 1. **Email Settings**: diff --git a/saml_auth/admin.py b/saml_auth/admin.py index 0f464dee..fc0d331e 100644 --- a/saml_auth/admin.py +++ b/saml_auth/admin.py @@ -51,7 +51,7 @@ class SAMLConfigurationAdmin(admin.ModelAdmin): search_fields = ['social_app__name', 'idp_id', 'sp_metadata_url'] fieldsets = [ - ('Provider Settings', {'fields': ['social_app', 'idp_id', 'idp_cert']}), + ('Provider Settings', {'fields': ['social_app', 'idp_id', 'idp_cert', 'sp_cert', 'sp_private_key']}), ('URLs', {'fields': ['sso_url', 'slo_url', 'sp_metadata_url']}), ('Group Management', {'fields': ['remove_from_groups', 'save_saml_response_logs']}), ('Attribute Mapping', {'fields': ['uid', 'name', 'email', 'groups', 'first_name', 'last_name', 'user_logo', 'role']}), diff --git a/saml_auth/custom/utils.py b/saml_auth/custom/utils.py index 6dca2934..0e84561b 100644 --- a/saml_auth/custom/utils.py +++ b/saml_auth/custom/utils.py @@ -53,16 +53,12 @@ def build_sp_config(request, provider_config, org): "binding": OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, }, } + if _sp_config.get("x509cert"): + sp_config["x509cert"] = _sp_config["x509cert"] + if _sp_config.get("private_key"): + sp_config["privateKey"] = _sp_config["private_key"] + avd = provider_config.get("advanced", {}) - if avd.get("x509cert") is not None: - sp_config["x509cert"] = avd["x509cert"] - - if avd.get("x509cert_new"): - sp_config["x509certNew"] = avd["x509cert_new"] - - if avd.get("private_key") is not None: - sp_config["privateKey"] = avd["private_key"] - if avd.get("name_id_format") is not None: sp_config["NameIDFormat"] = avd["name_id_format"] diff --git a/saml_auth/custom/views.py b/saml_auth/custom/views.py index 13bcce95..0f41b4b0 100644 --- a/saml_auth/custom/views.py +++ b/saml_auth/custom/views.py @@ -154,7 +154,9 @@ sls = SLSView.as_view() class MetadataView(SAMLViewMixin, View): def dispatch(self, request, organization_slug): provider = self.get_provider(organization_slug) - config = build_saml_config(self.request, provider.app.settings, organization_slug) + custom_configuration = provider.app.saml_configurations.first() + provider_config = custom_configuration.saml_provider_settings if custom_configuration else provider.app.settings + config = build_saml_config(self.request, provider_config, organization_slug) saml_settings = OneLogin_Saml2_Settings(settings=config, sp_validation_only=True) metadata = saml_settings.get_sp_metadata() errors = saml_settings.validate_metadata(metadata) diff --git a/saml_auth/migrations/0002_samlconfiguration_sp_cert_and_more.py b/saml_auth/migrations/0002_samlconfiguration_sp_cert_and_more.py new file mode 100644 index 00000000..07b5f216 --- /dev/null +++ b/saml_auth/migrations/0002_samlconfiguration_sp_cert_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.6 on 2026-05-31 12:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('saml_auth', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='samlconfiguration', + name='sp_cert', + field=models.TextField(blank=True, help_text='SP x509cert (PEM). Optional; required if SP private key is set.', null=True), + ), + migrations.AddField( + model_name='samlconfiguration', + name='sp_private_key', + field=models.TextField(blank=True, help_text='SP private key (PEM). Optional; required if SP certificate is set.', null=True), + ), + ] diff --git a/saml_auth/models.py b/saml_auth/models.py index 656c4725..be7d91cb 100644 --- a/saml_auth/models.py +++ b/saml_auth/models.py @@ -14,6 +14,8 @@ class SAMLConfiguration(models.Model): # Certificates idp_cert = models.TextField(help_text='x509cert') + sp_cert = models.TextField(blank=True, null=True, help_text='SP x509cert (PEM). Optional; required if SP private key is set.') + sp_private_key = models.TextField(blank=True, null=True, help_text='SP private key (PEM). Optional; required if SP certificate is set.') # Attribute Mapping Fields uid = models.CharField(max_length=100, help_text='eg eduPersonPrincipalName') @@ -49,6 +51,11 @@ class SAMLConfiguration(models.Model): if existing_conf.exists(): raise ValidationError({'social_app': 'Cannot create configuration for the same social app because one configuration already exists.'}) + if self.sp_cert and not self.sp_private_key: + raise ValidationError({'sp_private_key': 'Required when SP certificate is provided.'}) + if self.sp_private_key and not self.sp_cert: + raise ValidationError({'sp_cert': 'Required when SP private key is provided.'}) + super().clean() @property @@ -56,6 +63,10 @@ class SAMLConfiguration(models.Model): # provide settings in a way for Social App SAML provider provider_settings = {} provider_settings["sp"] = {"entity_id": self.sp_metadata_url} + if self.sp_cert: + provider_settings["sp"]["x509cert"] = self.sp_cert + if self.sp_private_key: + provider_settings["sp"]["private_key"] = self.sp_private_key provider_settings["idp"] = {"slo_url": self.slo_url, "sso_url": self.sso_url, "x509cert": self.idp_cert, "entity_id": self.idp_id} provider_settings["attribute_mapping"] = {