diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py
index 8443a6bf7..eaf31f176 100644
--- a/authentik/providers/saml/api.py
+++ b/authentik/providers/saml/api.py
@@ -22,6 +22,7 @@ class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer):
"assertion_valid_not_on_or_after",
"session_valid_not_on_or_after",
"property_mappings",
+ "name_id_mapping",
"digest_algorithm",
"signature_algorithm",
"signing_kp",
diff --git a/authentik/providers/saml/forms.py b/authentik/providers/saml/forms.py
index 9518f6494..d70b27b61 100644
--- a/authentik/providers/saml/forms.py
+++ b/authentik/providers/saml/forms.py
@@ -42,6 +42,7 @@ class SAMLProviderForm(forms.ModelForm):
"signing_kp",
"verification_kp",
"property_mappings",
+ "name_id_mapping",
"assertion_valid_not_before",
"assertion_valid_not_on_or_after",
"session_valid_not_on_or_after",
@@ -84,7 +85,9 @@ class SAMLPropertyMappingForm(forms.ModelForm):
"saml_name": mark_safe(
_(
"URN OID used by SAML. This is optional. "
- 'Reference'
+ 'Reference.'
+ " If this property mapping is used for NameID Property, "
+ "this field is discarded."
)
),
}
diff --git a/authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py b/authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py
new file mode 100644
index 000000000..7425f6ca4
--- /dev/null
+++ b/authentik/providers/saml/migrations/0011_samlprovider_name_id_mapping.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.1.4 on 2021-01-28 21:01
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_providers_saml", "0010_auto_20201230_2112"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="samlprovider",
+ name="name_id_mapping",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ help_text="Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be considered",
+ null=True,
+ verbose_name="NameID Property Mapping",
+ on_delete=django.db.models.deletion.SET_DEFAULT,
+ to="authentik_providers_saml.samlpropertymapping",
+ ),
+ ),
+ ]
diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py
index 2536456b0..74edfdc96 100644
--- a/authentik/providers/saml/models.py
+++ b/authentik/providers/saml/models.py
@@ -65,6 +65,21 @@ class SAMLProvider(Provider):
),
)
+ name_id_mapping = models.ForeignKey(
+ "SAMLPropertyMapping",
+ default=None,
+ blank=True,
+ null=True,
+ on_delete=models.SET_DEFAULT,
+ verbose_name=_("NameID Property Mapping"),
+ help_text=_(
+ (
+ "Configure how the NameID value will be created. When left empty, "
+ "the NameIDPolicy of the incoming request will be considered"
+ )
+ ),
+ )
+
assertion_valid_not_before = models.TextField(
default="minutes=-5",
validators=[timedelta_string_validator],
diff --git a/authentik/providers/saml/processors/assertion.py b/authentik/providers/saml/processors/assertion.py
index 9efaccc75..163f8f187 100644
--- a/authentik/providers/saml/processors/assertion.py
+++ b/authentik/providers/saml/processors/assertion.py
@@ -139,13 +139,30 @@ class AssertionProcessor:
audience.text = self.provider.audience
return conditions
+ # pylint: disable=too-many-return-statements
def get_name_id(self) -> Element:
"""Get NameID Element"""
name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID")
name_id.attrib["Format"] = self.auth_n_request.name_id_policy
+ # persistent is used as a fallback, so always generate it
persistent = sha256(
f"{self.http_request.user.id}-{settings.SECRET_KEY}".encode("ascii")
).hexdigest()
+ name_id.text = persistent
+ # If name_id_mapping is set, we override the value, regardless of what the SP asks for
+ if self.provider.name_id_mapping:
+ try:
+ value = self.provider.name_id_mapping.evaluate(
+ user=self.http_request.user,
+ request=self.http_request,
+ provider=self.provider,
+ )
+ if value is not None:
+ name_id.text = value
+ return name_id
+ except PropertyMappingExpressionException as exc:
+ LOGGER.warning(str(exc))
+ return name_id
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
name_id.text = self.http_request.user.email
return name_id
diff --git a/authentik/stages/email/tests/test_templates.py b/authentik/stages/email/tests/test_templates.py
index 3c2c06c93..a13344752 100644
--- a/authentik/stages/email/tests/test_templates.py
+++ b/authentik/stages/email/tests/test_templates.py
@@ -1,8 +1,10 @@
"""email tests"""
from os import unlink
from pathlib import Path
+from sys import platform
from tempfile import gettempdir, mkstemp
from typing import Any
+from unittest.case import skipUnless
from django.conf import settings
from django.test import TestCase
@@ -17,6 +19,7 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
return templates_setting
+@skipUnless(platform.startswith("linux"), "requires local docker")
class TestEmailStageTemplates(TestCase):
"""Email tests"""
diff --git a/swagger.yaml b/swagger.yaml
index 548a5e4cd..bd3b20848 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -8839,6 +8839,12 @@ definitions:
type: string
format: uuid
uniqueItems: true
+ name_id_mapping:
+ title: NameID Property Mapping
+ description: Configure how the NameID value will be created. When left empty,
+ the NameIDPolicy of the incoming request will be considered
+ type: string
+ x-nullable: true
digest_algorithm:
title: Digest algorithm
type: string