providers/saml: make NameID configurable using a Property Mapping
This commit is contained in:
parent
66a8b52c7c
commit
5ef4354723
|
@ -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",
|
||||||
|
|
|
@ -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."
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in New Issue