providers/saml: make NameID configurable using a Property Mapping

This commit is contained in:
Jens Langhammer 2021-01-28 22:50:13 +01:00
parent 66a8b52c7c
commit 5ef4354723
7 changed files with 73 additions and 1 deletions

View File

@ -22,6 +22,7 @@ class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer):
"assertion_valid_not_on_or_after", "assertion_valid_not_on_or_after",
"session_valid_not_on_or_after", "session_valid_not_on_or_after",
"property_mappings", "property_mappings",
"name_id_mapping",
"digest_algorithm", "digest_algorithm",
"signature_algorithm", "signature_algorithm",
"signing_kp", "signing_kp",

View File

@ -42,6 +42,7 @@ class SAMLProviderForm(forms.ModelForm):
"signing_kp", "signing_kp",
"verification_kp", "verification_kp",
"property_mappings", "property_mappings",
"name_id_mapping",
"assertion_valid_not_before", "assertion_valid_not_before",
"assertion_valid_not_on_or_after", "assertion_valid_not_on_or_after",
"session_valid_not_on_or_after", "session_valid_not_on_or_after",
@ -84,7 +85,9 @@ class SAMLPropertyMappingForm(forms.ModelForm):
"saml_name": mark_safe( "saml_name": mark_safe(
_( _(
"URN OID used by SAML. This is optional. " "URN OID used by SAML. This is optional. "
'<a href="https://www.rfc-editor.org/rfc/rfc2798.html#section-2">Reference</a>' '<a href="https://www.rfc-editor.org/rfc/rfc2798.html#section-2">Reference</a>.'
" If this property mapping is used for NameID Property, "
"this field is discarded."
) )
), ),
} }

View File

@ -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",
),
),
]

View File

@ -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( assertion_valid_not_before = models.TextField(
default="minutes=-5", default="minutes=-5",
validators=[timedelta_string_validator], validators=[timedelta_string_validator],

View File

@ -139,13 +139,30 @@ class AssertionProcessor:
audience.text = self.provider.audience audience.text = self.provider.audience
return conditions return conditions
# pylint: disable=too-many-return-statements
def get_name_id(self) -> Element: def get_name_id(self) -> Element:
"""Get NameID Element""" """Get NameID Element"""
name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID") name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID")
name_id.attrib["Format"] = self.auth_n_request.name_id_policy name_id.attrib["Format"] = self.auth_n_request.name_id_policy
# persistent is used as a fallback, so always generate it
persistent = sha256( persistent = sha256(
f"{self.http_request.user.id}-{settings.SECRET_KEY}".encode("ascii") f"{self.http_request.user.id}-{settings.SECRET_KEY}".encode("ascii")
).hexdigest() ).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: if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
name_id.text = self.http_request.user.email name_id.text = self.http_request.user.email
return name_id return name_id

View File

@ -1,8 +1,10 @@
"""email tests""" """email tests"""
from os import unlink from os import unlink
from pathlib import Path from pathlib import Path
from sys import platform
from tempfile import gettempdir, mkstemp from tempfile import gettempdir, mkstemp
from typing import Any from typing import Any
from unittest.case import skipUnless
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
@ -17,6 +19,7 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
return templates_setting return templates_setting
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestEmailStageTemplates(TestCase): class TestEmailStageTemplates(TestCase):
"""Email tests""" """Email tests"""

View File

@ -8839,6 +8839,12 @@ definitions:
type: string type: string
format: uuid format: uuid
uniqueItems: true 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: digest_algorithm:
title: Digest algorithm title: Digest algorithm
type: string type: string