From a2ca19d718105dcaf1f76097e929a23d0a7db89e Mon Sep 17 00:00:00 2001 From: Connor Peshek Date: Tue, 28 Apr 2026 17:31:12 -0500 Subject: [PATCH] providers/saml: generate issuer url when provider is set on app (#18022) * providers/saml: generate issuer url in saml processors unless overridded * remove issuer * remove duplicate * Generate url when assertion is created and save to session * cleanup * Fix front-end rendering of issuer * Update web/src/admin/providers/saml/SAMLProviderViewPage.ts Co-authored-by: Jens L. Signed-off-by: Connor Peshek * Update authentik/providers/saml/models.py Co-authored-by: Jens L. Signed-off-by: Connor Peshek * Update authentik/providers/saml/models.py Co-authored-by: Jens L. Signed-off-by: Connor Peshek * use reverse for urls and update tests * update issuer description * Don't absorb sp entity id * rename issuer_url to issuer_override * fix migration file to rename to override * fix migration file order * lint, fix tests * fix tests * fix once again not importing the sp issuer * build * use const for default issuer --------- Signed-off-by: Connor Peshek Co-authored-by: connor peshek Co-authored-by: Jens L. --- authentik/common/saml/constants.py | 2 ++ authentik/providers/saml/api/providers.py | 27 +++++++++++++-- ...022_remove_samlprovider_issuer_and_more.py | 34 +++++++++++++++++++ authentik/providers/saml/models.py | 12 ++++++- .../providers/saml/processors/assertion.py | 18 +++++++++- .../saml/processors/logout_request.py | 16 +++++++-- .../processors/logout_response_processor.py | 16 +++++++-- .../providers/saml/processors/metadata.py | 15 +++++++- .../saml/processors/metadata_parser.py | 1 - authentik/providers/saml/signals.py | 4 +++ authentik/providers/saml/tasks.py | 4 +++ .../saml/tests/test_auth_n_request.py | 10 ++++-- .../providers/saml/tests/test_idp_logout.py | 10 +++--- .../tests/test_logout_processor_and_parser.py | 16 ++++----- .../tests/test_logout_request_processor.py | 2 +- .../tests/test_logout_response_processor.py | 28 ++++++++++++--- .../providers/saml/tests/test_metadata.py | 2 -- .../saml/tests/test_models_session.py | 19 +++++++++-- authentik/providers/saml/tests/test_schema.py | 6 ++++ authentik/providers/saml/tests/test_tasks.py | 9 +++-- .../providers/saml/tests/test_views_sp_slo.py | 12 +++---- authentik/providers/saml/urls.py | 6 ++++ authentik/providers/saml/views/flows.py | 1 + authentik/providers/saml/views/sp_slo.py | 15 ++++++++ blueprints/schema.json | 7 ++-- packages/client-ts/src/apis/ProvidersApi.ts | 12 +++---- .../src/models/PatchedSAMLProviderRequest.ts | 8 ++--- packages/client-ts/src/models/SAMLProvider.ts | 17 +++++++--- .../src/models/SAMLProviderRequest.ts | 8 ++--- schema.yml | 26 ++++++++------ tests/e2e/test_provider_saml.py | 16 ++++----- .../steps/SubmitStepOverviewRenderers.ts | 2 +- .../providers/saml/SAMLProviderFormForm.ts | 18 +++++----- .../providers/saml/SAMLProviderViewPage.ts | 4 +-- 34 files changed, 307 insertions(+), 96 deletions(-) create mode 100644 authentik/providers/saml/migrations/0022_remove_samlprovider_issuer_and_more.py diff --git a/authentik/common/saml/constants.py b/authentik/common/saml/constants.py index a2342ba056..ff4898d70c 100644 --- a/authentik/common/saml/constants.py +++ b/authentik/common/saml/constants.py @@ -30,6 +30,8 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" SAML_STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success" +DEFAULT_ISSUER = "authentik" + DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1" RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" # https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2 diff --git a/authentik/providers/saml/api/providers.py b/authentik/providers/saml/api/providers.py index e15cf96c5d..209b37acde 100644 --- a/authentik/providers/saml/api/providers.py +++ b/authentik/providers/saml/api/providers.py @@ -24,7 +24,11 @@ from rest_framework.viewsets import ModelViewSet from structlog.stdlib import get_logger from authentik.api.validation import validate -from authentik.common.saml.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT +from authentik.common.saml.constants import ( + DEFAULT_ISSUER, + SAML_BINDING_POST, + SAML_BINDING_REDIRECT, +) from authentik.core.api.providers import ProviderSerializer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer @@ -55,6 +59,7 @@ class SAMLProviderSerializer(ProviderSerializer): """SAMLProvider Serializer""" url_download_metadata = SerializerMethodField() + url_issuer = SerializerMethodField() url_sso_post = SerializerMethodField() url_sso_redirect = SerializerMethodField() @@ -85,6 +90,23 @@ class SAMLProviderSerializer(ProviderSerializer): + "?download" ) + def get_url_issuer(self, instance: SAMLProvider) -> str: + """Get Issuer/EntityID URL""" + if instance.issuer_override: + return instance.issuer_override + if "request" not in self._context: + return DEFAULT_ISSUER + request: HttpRequest = self._context["request"]._request + try: + return request.build_absolute_uri( + reverse( + "authentik_providers_saml:base", + kwargs={"application_slug": instance.application.slug}, + ) + ) + except Provider.application.RelatedObjectDoesNotExist: + return DEFAULT_ISSUER + def get_url_sso_post(self, instance: SAMLProvider) -> str: """Get SSO Post URL""" if "request" not in self._context: @@ -198,7 +220,7 @@ class SAMLProviderSerializer(ProviderSerializer): "acs_url", "sls_url", "audience", - "issuer", + "issuer_override", "assertion_valid_not_before", "assertion_valid_not_on_or_after", "session_valid_not_on_or_after", @@ -220,6 +242,7 @@ class SAMLProviderSerializer(ProviderSerializer): "default_relay_state", "default_name_id_policy", "url_download_metadata", + "url_issuer", "url_sso_post", "url_sso_redirect", "url_sso_init", diff --git a/authentik/providers/saml/migrations/0022_remove_samlprovider_issuer_and_more.py b/authentik/providers/saml/migrations/0022_remove_samlprovider_issuer_and_more.py new file mode 100644 index 0000000000..aa38866566 --- /dev/null +++ b/authentik/providers/saml/migrations/0022_remove_samlprovider_issuer_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.11 on 2026-02-24 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_providers_saml", "0021_samlprovider_sign_logout_response"), + ] + + operations = [ + migrations.RenameField( + model_name="samlprovider", + old_name="issuer", + new_name="issuer_override", + ), + migrations.AlterField( + model_name="samlprovider", + name="issuer_override", + field=models.TextField( + blank=True, + default="", + help_text="Also known as EntityID. Providing a value overrides the default issuer generated by authentik.", + ), + ), + migrations.AddField( + model_name="samlsession", + name="issuer", + field=models.TextField( + default=None, help_text="SAML Issuer used for this session", null=True + ), + ), + ] diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py index eeb91fc5b9..f73534356b 100644 --- a/authentik/providers/saml/models.py +++ b/authentik/providers/saml/models.py @@ -77,7 +77,14 @@ class SAMLProvider(Provider): "no audience restriction will be added." ), ) - issuer = models.TextField(help_text=_("Also known as EntityID"), default="authentik") + issuer_override = models.TextField( + blank=True, + default="", + help_text=_( + "Also known as EntityID. Providing a value overrides the default issuer " + "generated by authentik." + ), + ) sls_url = models.TextField( blank=True, validators=[DomainlessURLValidator(schemes=("http", "https"))], @@ -318,6 +325,9 @@ class SAMLSession(InternallyManagedMixin, SerializerModel, ExpiringModel): session_index = models.TextField(help_text=_("SAML SessionIndex for this session")) name_id = models.TextField(help_text=_("SAML NameID value for this session")) name_id_format = models.TextField(default="", blank=True, help_text=_("SAML NameID format")) + issuer = models.TextField( + default=None, null=True, help_text=_("SAML Issuer used for this session") + ) created = models.DateTimeField(auto_now_add=True) @property diff --git a/authentik/providers/saml/processors/assertion.py b/authentik/providers/saml/processors/assertion.py index 9cdbe2c344..21d519ace0 100644 --- a/authentik/providers/saml/processors/assertion.py +++ b/authentik/providers/saml/processors/assertion.py @@ -6,6 +6,7 @@ from types import GeneratorType import xmlsec from django.http import HttpRequest +from django.urls import reverse from django.utils.timezone import now from lxml import etree # nosec from lxml.etree import Element, SubElement, _Element # nosec @@ -63,6 +64,7 @@ class AssertionProcessor: session_index: str name_id: str name_id_format: str + issuer: str session_not_on_or_after_datetime: datetime def __init__(self, provider: SAMLProvider, request: HttpRequest, auth_n_request: AuthNRequest): @@ -137,10 +139,24 @@ class AssertionProcessor: continue return attribute_statement + def _get_issuer_value(self) -> str: + """Get issuer value, with fallback to generated URL if empty""" + # If user has set an override issuer, use it + if self.provider.issuer_override: + return self.provider.issuer_override + + return self.http_request.build_absolute_uri( + reverse( + "authentik_providers_saml:base", + kwargs={"application_slug": self.provider.application.slug}, + ) + ) + def get_issuer(self) -> Element: """Get Issuer Element""" issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer", nsmap=NS_MAP) - issuer.text = self.provider.issuer + self.issuer = self._get_issuer_value() + issuer.text = self.issuer return issuer def get_assertion_auth_n_statement(self) -> Element: diff --git a/authentik/providers/saml/processors/logout_request.py b/authentik/providers/saml/processors/logout_request.py index 1f7114db1f..5b8d529059 100644 --- a/authentik/providers/saml/processors/logout_request.py +++ b/authentik/providers/saml/processors/logout_request.py @@ -8,6 +8,7 @@ from lxml import etree # nosec from lxml.etree import Element, _Element from authentik.common.saml.constants import ( + DEFAULT_ISSUER, DIGEST_ALGORITHM_TRANSLATION_MAP, NS_MAP, NS_SAML_ASSERTION, @@ -33,11 +34,12 @@ class LogoutRequestProcessor: name_id_format: str session_index: str | None relay_state: str | None + issuer: str | None _issue_instant: str _request_id: str - def __init__( + def __init__( # noqa: PLR0913 self, provider: SAMLProvider, user: User | None, @@ -46,6 +48,7 @@ class LogoutRequestProcessor: name_id_format: str = SAML_NAME_ID_FORMAT_EMAIL, session_index: str | None = None, relay_state: str | None = None, + issuer: str | None = None, ): self.provider = provider self.user = user @@ -54,14 +57,23 @@ class LogoutRequestProcessor: self.name_id_format = name_id_format self.session_index = session_index self.relay_state = relay_state + self.issuer = issuer self._issue_instant = get_time_string() self._request_id = get_random_id() + def _get_issuer_value(self) -> str: + """Get issuer value from session, with fallback to provider""" + if self.issuer: + return self.issuer + if self.provider.issuer_override: + return self.provider.issuer_override + return DEFAULT_ISSUER + def get_issuer(self) -> Element: """Get Issuer element""" issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer") - issuer.text = self.provider.issuer + issuer.text = self._get_issuer_value() return issuer def get_name_id(self) -> Element: diff --git a/authentik/providers/saml/processors/logout_response_processor.py b/authentik/providers/saml/processors/logout_response_processor.py index d97ad54eb9..e47b30804a 100644 --- a/authentik/providers/saml/processors/logout_response_processor.py +++ b/authentik/providers/saml/processors/logout_response_processor.py @@ -8,6 +8,7 @@ from lxml import etree from lxml.etree import Element, SubElement from authentik.common.saml.constants import ( + DEFAULT_ISSUER, DIGEST_ALGORITHM_TRANSLATION_MAP, NS_MAP, NS_SAML_ASSERTION, @@ -28,27 +29,38 @@ class LogoutResponseProcessor: logout_request: LogoutRequest destination: str | None relay_state: str | None + issuer: str | None _issue_instant: str _response_id: str - def __init__( + def __init__( # noqa: PLR0913 self, provider: SAMLProvider, logout_request: LogoutRequest, destination: str | None = None, relay_state: str | None = None, + issuer: str | None = None, ): self.provider = provider self.logout_request = logout_request self.destination = destination self.relay_state = relay_state or (logout_request.relay_state if logout_request else None) + self.issuer = issuer self._issue_instant = get_time_string() self._response_id = get_random_id() + def _get_issuer_value(self) -> str: + """Get issuer value from session, with fallback to provider""" + if self.issuer: + return self.issuer + if self.provider.issuer_override: + return self.provider.issuer_override + return DEFAULT_ISSUER + def get_issuer(self) -> Element: """Get Issuer element""" issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer") - issuer.text = self.provider.issuer + issuer.text = self._get_issuer_value() return issuer def build(self, status: str = "Success") -> Element: diff --git a/authentik/providers/saml/processors/metadata.py b/authentik/providers/saml/processors/metadata.py index f7e694045d..eba807e0f8 100644 --- a/authentik/providers/saml/processors/metadata.py +++ b/authentik/providers/saml/processors/metadata.py @@ -40,6 +40,19 @@ class MetadataProcessor: self.force_binding = None self.xml_id = "_" + sha256(f"{provider.name}-{provider.pk}".encode("ascii")).hexdigest() + def _get_issuer_value(self) -> str: + """Get issuer value, with fallback to generated URL if empty""" + # If user has set an override issuer, use it + if self.provider.issuer_override: + return self.provider.issuer_override + + return self.http_request.build_absolute_uri( + reverse( + "authentik_providers_saml:base", + kwargs={"application_slug": self.provider.application.slug}, + ) + ) + # Using type unions doesn't work with cython types (which is what lxml is) def get_signing_key_descriptor(self) -> Element | None: """Get Signing KeyDescriptor, if enabled for the provider""" @@ -189,7 +202,7 @@ class MetadataProcessor: """Build full EntityDescriptor""" entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP) entity_descriptor.attrib["ID"] = self.xml_id - entity_descriptor.attrib["entityID"] = self.provider.issuer + entity_descriptor.attrib["entityID"] = self._get_issuer_value() if self.provider.signing_kp: self._prepare_signature(entity_descriptor) diff --git a/authentik/providers/saml/processors/metadata_parser.py b/authentik/providers/saml/processors/metadata_parser.py index eae12a3244..28491fb8e7 100644 --- a/authentik/providers/saml/processors/metadata_parser.py +++ b/authentik/providers/saml/processors/metadata_parser.py @@ -51,7 +51,6 @@ class ServiceProviderMetadata: provider = SAMLProvider.objects.create( name=name, authorization_flow=authorization_flow, invalidation_flow=invalidation_flow ) - provider.issuer = self.entity_id provider.sp_binding = self.acs_binding provider.acs_url = self.acs_location provider.default_name_id_policy = self.name_id_policy diff --git a/authentik/providers/saml/signals.py b/authentik/providers/saml/signals.py index 4983860975..6c71deba5c 100644 --- a/authentik/providers/saml/signals.py +++ b/authentik/providers/saml/signals.py @@ -75,6 +75,7 @@ def handle_saml_iframe_pre_user_logout( name_id_format=session.name_id_format, session_index=session.session_index, relay_state=relay_state, + issuer=session.issuer, ) if session.provider.sls_binding == SAMLBindings.POST: @@ -163,6 +164,7 @@ def handle_flow_pre_user_logout( name_id_format=session.name_id_format, session_index=session.session_index, relay_state=relay_state, + issuer=session.issuer, ) if session.provider.sls_binding == SAMLBindings.POST: @@ -224,6 +226,7 @@ def user_session_deleted_saml_logout(sender, instance: AuthenticatedSession, **_ name_id=saml_session.name_id, name_id_format=saml_session.name_id_format, session_index=saml_session.session_index, + issuer=saml_session.issuer, ) @@ -257,4 +260,5 @@ def user_deactivated_saml_logout(sender, instance: User, **kwargs): name_id=saml_session.name_id, name_id_format=saml_session.name_id_format, session_index=saml_session.session_index, + issuer=saml_session.issuer, ) diff --git a/authentik/providers/saml/tasks.py b/authentik/providers/saml/tasks.py index 84b87d20b0..6fd328c55f 100644 --- a/authentik/providers/saml/tasks.py +++ b/authentik/providers/saml/tasks.py @@ -22,6 +22,7 @@ def send_saml_logout_request( name_id: str, name_id_format: str, session_index: str, + issuer: str, ): """Send SAML LogoutRequest to a Service Provider using session data""" provider = SAMLProvider.objects.filter(pk=provider_pk).first() @@ -47,6 +48,7 @@ def send_saml_logout_request( name_id=name_id, name_id_format=name_id_format, session_index=session_index, + issuer=issuer, ) return send_post_logout_request(provider, processor) @@ -89,6 +91,7 @@ def send_saml_logout_response( sls_url: str, logout_request_id: str | None = None, relay_state: str | None = None, + issuer: str | None = None, ): """Send SAML LogoutResponse to a Service Provider using backchannel (server-to-server)""" provider = SAMLProvider.objects.filter(pk=provider_pk).first() @@ -119,6 +122,7 @@ def send_saml_logout_response( logout_request=logout_request, destination=sls_url, relay_state=relay_state, + issuer=issuer, ) encoded_response = processor.encode_post() diff --git a/authentik/providers/saml/tests/test_auth_n_request.py b/authentik/providers/saml/tests/test_auth_n_request.py index 5cc19fd1bb..38152c729d 100644 --- a/authentik/providers/saml/tests/test_auth_n_request.py +++ b/authentik/providers/saml/tests/test_auth_n_request.py @@ -15,6 +15,7 @@ from authentik.common.saml.constants import ( SAML_NAME_ID_FORMAT_EMAIL, SAML_NAME_ID_FORMAT_UNSPECIFIED, ) +from authentik.core.models import Application from authentik.core.tests.utils import ( RequestFactory, create_test_admin_user, @@ -97,6 +98,11 @@ class TestAuthNRequest(TestCase): ) self.provider.property_mappings.set(SAMLPropertyMapping.objects.all()) self.provider.save() + Application.objects.create( + name="test-app", + slug="test-app", + provider=self.provider, + ) self.source = SAMLSource.objects.create( slug="provider", issuer="authentik", @@ -526,7 +532,7 @@ class TestAuthNRequest(TestCase): authorization_flow=create_test_flow(), acs_url="https://10.120.20.200/saml-sp/SAML2/POST", audience="https://10.120.20.200/saml-sp/SAML2/POST", - issuer="https://10.120.20.200/saml-sp/SAML2/POST", + issuer_override="https://10.120.20.200/saml-sp/SAML2/POST", signing_kp=static_keypair, verification_kp=static_keypair, ) @@ -547,7 +553,7 @@ class TestAuthNRequest(TestCase): "saml/acs/2d737f96-55fb-4035-953e-5e24134eb778" ), audience="https://10.120.20.200/saml-sp/SAML2/POST", - issuer="https://10.120.20.200/saml-sp/SAML2/POST", + issuer_override="https://10.120.20.200/saml-sp/SAML2/POST", signing_kp=create_test_cert(), ) parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST) diff --git a/authentik/providers/saml/tests/test_idp_logout.py b/authentik/providers/saml/tests/test_idp_logout.py index 077d680ae9..c9631899cd 100644 --- a/authentik/providers/saml/tests/test_idp_logout.py +++ b/authentik/providers/saml/tests/test_idp_logout.py @@ -47,7 +47,7 @@ class TestNativeLogoutStageView(TestCase): authorization_flow=self.flow, acs_url="https://sp1.example.com/acs", sls_url="https://sp1.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", logout_method=SAMLLogoutMethods.FRONTCHANNEL_NATIVE, @@ -58,7 +58,7 @@ class TestNativeLogoutStageView(TestCase): authorization_flow=self.flow, acs_url="https://sp2.example.com/acs", sls_url="https://sp2.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="post", sls_binding="post", logout_method=SAMLLogoutMethods.FRONTCHANNEL_NATIVE, @@ -218,7 +218,7 @@ class TestIframeLogoutStageView(TestCase): authorization_flow=self.flow, acs_url="https://sp1.example.com/acs", sls_url="https://sp1.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", logout_method="frontchannel_iframe", @@ -229,7 +229,7 @@ class TestIframeLogoutStageView(TestCase): authorization_flow=self.flow, acs_url="https://sp2.example.com/acs", sls_url="https://sp2.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="post", sls_binding="post", logout_method="frontchannel_iframe", @@ -372,7 +372,7 @@ class TestIdPLogoutIntegration(FlowTestCase): authorization_flow=self.flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", signing_kp=self.keypair, diff --git a/authentik/providers/saml/tests/test_logout_processor_and_parser.py b/authentik/providers/saml/tests/test_logout_processor_and_parser.py index 954bb55179..4a35676fcc 100644 --- a/authentik/providers/saml/tests/test_logout_processor_and_parser.py +++ b/authentik/providers/saml/tests/test_logout_processor_and_parser.py @@ -28,7 +28,7 @@ class TestLogoutIntegration(TestCase): authorization_flow=self.flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", signature_algorithm=RSA_SHA256, @@ -57,7 +57,7 @@ class TestLogoutIntegration(TestCase): parsed = self.parser.parse(encoded) # Verify all fields match - self.assertEqual(parsed.issuer, self.provider.issuer) + self.assertEqual(parsed.issuer, self.provider.issuer_override) self.assertEqual(parsed.name_id, "test@example.com") self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL) self.assertEqual(parsed.session_index, "test-session-123") @@ -72,7 +72,7 @@ class TestLogoutIntegration(TestCase): parsed = self.parser.parse_detached(encoded) # Verify all fields match - self.assertEqual(parsed.issuer, self.provider.issuer) + self.assertEqual(parsed.issuer, self.provider.issuer_override) self.assertEqual(parsed.name_id, "test@example.com") self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL) self.assertEqual(parsed.session_index, "test-session-123") @@ -106,7 +106,7 @@ class TestLogoutIntegration(TestCase): parsed = parser.parse(encoded) # Verify all fields match - self.assertEqual(parsed.issuer, self.provider.issuer) + self.assertEqual(parsed.issuer, self.provider.issuer_override) self.assertEqual(parsed.name_id, "signed@example.com") self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL) self.assertEqual(parsed.session_index, "signed-session-456") @@ -125,7 +125,7 @@ class TestLogoutIntegration(TestCase): parsed = self.parser.parse_detached(saml_request) # Verify parsing succeeded - self.assertEqual(parsed.issuer, self.provider.issuer) + self.assertEqual(parsed.issuer, self.provider.issuer_override) self.assertEqual(parsed.name_id, "test@example.com") self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL) @@ -164,7 +164,7 @@ class TestLogoutIntegration(TestCase): # Parse the SAMLRequest (unsigned XML) parsed = self.parser.parse_detached(params["SAMLRequest"][0]) - self.assertEqual(parsed.issuer, self.provider.issuer) + self.assertEqual(parsed.issuer, self.provider.issuer_override) def test_form_data_can_be_parsed(self): """Test that form data generates parseable POST request""" @@ -175,7 +175,7 @@ class TestLogoutIntegration(TestCase): parsed = self.parser.parse(form_data["SAMLRequest"]) # Verify parsing succeeded - self.assertEqual(parsed.issuer, self.provider.issuer) + self.assertEqual(parsed.issuer, self.provider.issuer_override) self.assertEqual(parsed.name_id, "test@example.com") self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL) self.assertEqual(parsed.session_index, "test-session-123") @@ -244,4 +244,4 @@ class TestLogoutIntegration(TestCase): # But same issuer self.assertEqual(parsed1.issuer, parsed2.issuer) - self.assertEqual(parsed1.issuer, self.provider.issuer) + self.assertEqual(parsed1.issuer, self.provider.issuer_override) diff --git a/authentik/providers/saml/tests/test_logout_request_processor.py b/authentik/providers/saml/tests/test_logout_request_processor.py index c73e53402b..f47483e2c0 100644 --- a/authentik/providers/saml/tests/test_logout_request_processor.py +++ b/authentik/providers/saml/tests/test_logout_request_processor.py @@ -35,7 +35,7 @@ class TestLogoutRequestProcessor(TestCase): authorization_flow=self.flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", signature_algorithm=RSA_SHA256, diff --git a/authentik/providers/saml/tests/test_logout_response_processor.py b/authentik/providers/saml/tests/test_logout_response_processor.py index 0503020423..88efd8086e 100644 --- a/authentik/providers/saml/tests/test_logout_response_processor.py +++ b/authentik/providers/saml/tests/test_logout_response_processor.py @@ -1,7 +1,7 @@ """logout response tests""" from defusedxml import ElementTree -from django.test import TestCase +from django.test import RequestFactory, TestCase from authentik.blueprints.tests import apply_blueprint from authentik.common.saml.constants import ( @@ -9,10 +9,13 @@ from authentik.common.saml.constants import ( NS_SAML_PROTOCOL, NS_SIGNATURE, ) +from authentik.core.models import Application from authentik.core.tests.utils import create_test_cert, create_test_flow +from authentik.lib.generators import generate_id from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.processors.logout_request_parser import LogoutRequest from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor +from authentik.providers.saml.processors.metadata import MetadataProcessor class TestLogoutResponse(TestCase): @@ -21,6 +24,7 @@ class TestLogoutResponse(TestCase): @apply_blueprint("system/providers-saml.yaml") def setUp(self): cert = create_test_cert() + self.factory = RequestFactory() self.provider: SAMLProvider = SAMLProvider.objects.create( authorization_flow=create_test_flow(), acs_url="http://testserver/source/saml/provider/acs/", @@ -30,17 +34,31 @@ class TestLogoutResponse(TestCase): ) self.provider.property_mappings.set(SAMLPropertyMapping.objects.all()) self.provider.save() + self.application = Application.objects.create( + name=generate_id(), + slug=generate_id(), + provider=self.provider, + ) def test_build_response(self): - """Test building a LogoutResponse""" + """Test building a LogoutResponse uses the generated issuer from the assertion""" + # Generate the issuer the same way the assertion/metadata processors would + request = self.factory.get("/") + metadata_processor = MetadataProcessor(self.provider, request) + generated_issuer = metadata_processor._get_issuer_value() + logout_request = LogoutRequest( id="test-request-id", issuer="test-sp", relay_state="test-relay-state", ) + # Pass the generated issuer as if it came from SAMLSession.issuer processor = LogoutResponseProcessor( - self.provider, logout_request, destination=self.provider.sls_url + self.provider, + logout_request, + destination=self.provider.sls_url, + issuer=generated_issuer, ) response_xml = processor.build_response(status="Success") @@ -51,9 +69,9 @@ class TestLogoutResponse(TestCase): self.assertEqual(root.attrib["Destination"], self.provider.sls_url) self.assertEqual(root.attrib["InResponseTo"], "test-request-id") - # Check Issuer + # Check Issuer matches the generated issuer from the assertion processor issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer") - self.assertEqual(issuer.text, self.provider.issuer) + self.assertEqual(issuer.text, generated_issuer) # Check Status status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode") diff --git a/authentik/providers/saml/tests/test_metadata.py b/authentik/providers/saml/tests/test_metadata.py index ab70f7f09a..7f66a9d7b8 100644 --- a/authentik/providers/saml/tests/test_metadata.py +++ b/authentik/providers/saml/tests/test_metadata.py @@ -85,7 +85,6 @@ class TestServiceProviderMetadataParser(TestCase): metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml")) provider = metadata.to_provider("test", self.flow, self.flow) self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs") - self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata") self.assertEqual(provider.sp_binding, SAMLBindings.POST) self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL) self.assertEqual( @@ -99,7 +98,6 @@ class TestServiceProviderMetadataParser(TestCase): metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml")) provider = metadata.to_provider("test", self.flow, self.flow) self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs") - self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata") self.assertEqual(provider.sp_binding, SAMLBindings.POST) self.assertEqual( provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem") diff --git a/authentik/providers/saml/tests/test_models_session.py b/authentik/providers/saml/tests/test_models_session.py index be53d87575..53a859bff9 100644 --- a/authentik/providers/saml/tests/test_models_session.py +++ b/authentik/providers/saml/tests/test_models_session.py @@ -32,7 +32,7 @@ class TestSAMLSessionModel(TestCase): name="test-provider", authorization_flow=self.flow, acs_url="https://sp.example.com/acs", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", ) # Create another provider for testing @@ -40,7 +40,7 @@ class TestSAMLSessionModel(TestCase): name="test-provider-2", authorization_flow=self.flow, acs_url="https://sp2.example.com/acs", - issuer="https://idp2.example.com", + issuer_override="https://idp2.example.com", ) # Create a session first (using authentik's custom Session model) @@ -72,6 +72,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Verify the session was created @@ -100,6 +101,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Try to create another session with same session_index and provider @@ -113,6 +115,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) def test_cascade_deletion_user(self): @@ -127,6 +130,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Verify session exists @@ -150,6 +154,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Verify session exists @@ -173,6 +178,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Verify session exists @@ -196,6 +202,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Create second session with different provider @@ -208,6 +215,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Verify both sessions exist @@ -229,6 +237,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=future_time, expiring=True, + issuer="authentik", ) # Verify expiry time @@ -248,6 +257,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=past_time, expiring=True, + issuer="authentik", ) # Check if marked as expired @@ -265,6 +275,7 @@ class TestSAMLSessionModel(TestCase): name_id_format="", # Blank format expires=self.expires, expiring=True, + issuer="authentik", ) # Verify it was created successfully @@ -283,6 +294,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) session2 = SAMLSession.objects.create( @@ -294,6 +306,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Query by provider @@ -316,6 +329,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Check serializer property @@ -334,6 +348,7 @@ class TestSAMLSessionModel(TestCase): name_id_format=self.name_id_format, expires=self.expires, expiring=True, + issuer="authentik", ) # Verify sessions exist diff --git a/authentik/providers/saml/tests/test_schema.py b/authentik/providers/saml/tests/test_schema.py index 0d4ca5b2f1..01c3a9b1dc 100644 --- a/authentik/providers/saml/tests/test_schema.py +++ b/authentik/providers/saml/tests/test_schema.py @@ -7,6 +7,7 @@ from guardian.shortcuts import get_anonymous_user from lxml import etree # nosec from authentik.blueprints.tests import apply_blueprint +from authentik.core.models import Application from authentik.core.tests.utils import RequestFactory, create_test_cert, create_test_flow from authentik.lib.xml import lxml_from_string from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider @@ -30,6 +31,11 @@ class TestSchema(TestCase): ) self.provider.property_mappings.set(SAMLPropertyMapping.objects.all()) self.provider.save() + Application.objects.create( + name="test-app", + slug="test-app", + provider=self.provider, + ) self.source = SAMLSource.objects.create( slug="provider", issuer="authentik", diff --git a/authentik/providers/saml/tests/test_tasks.py b/authentik/providers/saml/tests/test_tasks.py index 0201cec56d..a0e77a10f9 100644 --- a/authentik/providers/saml/tests/test_tasks.py +++ b/authentik/providers/saml/tests/test_tasks.py @@ -28,7 +28,7 @@ class TestSendSamlLogoutResponse(TestCase): authorization_flow=self.flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", signing_kp=self.cert, ) @@ -137,7 +137,7 @@ class TestSendSamlLogoutRequest(TestCase): authorization_flow=self.flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", signing_kp=self.cert, ) @@ -155,6 +155,7 @@ class TestSendSamlLogoutRequest(TestCase): name_id="test@example.com", name_id_format=SAML_NAME_ID_FORMAT_EMAIL, session_index="test-session-123", + issuer="https://idp.example.com", ) self.assertTrue(result) @@ -179,6 +180,7 @@ class TestSendSamlLogoutRequest(TestCase): name_id="test@example.com", name_id_format=SAML_NAME_ID_FORMAT_EMAIL, session_index="test-session-123", + issuer="https://idp.example.com", ) self.assertFalse(result) @@ -198,6 +200,7 @@ class TestSendSamlLogoutRequest(TestCase): name_id="test@example.com", name_id_format=SAML_NAME_ID_FORMAT_EMAIL, session_index="test-session-123", + issuer="https://idp.example.com", ) @@ -214,7 +217,7 @@ class TestSendPostLogoutRequest(TestCase): authorization_flow=self.flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", signing_kp=self.cert, ) diff --git a/authentik/providers/saml/tests/test_views_sp_slo.py b/authentik/providers/saml/tests/test_views_sp_slo.py index 0408fcbb1f..c2baef5d8b 100644 --- a/authentik/providers/saml/tests/test_views_sp_slo.py +++ b/authentik/providers/saml/tests/test_views_sp_slo.py @@ -40,7 +40,7 @@ class TestSPInitiatedSLOViews(TestCase): invalidation_flow=self.invalidation_flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", ) @@ -90,7 +90,7 @@ class TestSPInitiatedSLOViews(TestCase): # Verify logout request was stored in plan context self.assertIn("authentik/providers/saml/logout_request", view.plan_context) logout_request = view.plan_context["authentik/providers/saml/logout_request"] - self.assertEqual(logout_request.issuer, self.provider.issuer) + self.assertEqual(logout_request.issuer, self.provider.issuer_override) self.assertEqual(logout_request.session_index, "test-session-123") def test_redirect_view_handles_logout_response_with_plan_context(self): @@ -228,7 +228,7 @@ class TestSPInitiatedSLOViews(TestCase): # Verify logout request was stored in plan context self.assertIn("authentik/providers/saml/logout_request", view.plan_context) logout_request = view.plan_context["authentik/providers/saml/logout_request"] - self.assertEqual(logout_request.issuer, self.provider.issuer) + self.assertEqual(logout_request.issuer, self.provider.issuer_override) self.assertEqual(logout_request.session_index, "test-session-123") def test_post_view_handles_logout_response_with_plan_context(self): @@ -396,7 +396,7 @@ class TestSPInitiatedSLOViews(TestCase): authorization_flow=self.flow, acs_url="https://sp2.example.com/acs", sls_url="https://sp2.example.com/sls", - issuer="https://idp2.example.com", + issuer_override="https://idp2.example.com", invalidation_flow=None, # No invalidation flow ) @@ -524,7 +524,7 @@ class TestSPInitiatedSLOLogoutMethods(TestCase): invalidation_flow=self.invalidation_flow, acs_url="https://sp.example.com/acs", sls_url="https://sp.example.com/sls", - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", sp_binding="redirect", sls_binding="redirect", signing_kp=self.cert, @@ -714,7 +714,7 @@ class TestSPInitiatedSLOLogoutMethods(TestCase): invalidation_flow=self.invalidation_flow, acs_url="https://sp.example.com/acs", sls_url="", # No SLS URL - issuer="https://idp.example.com", + issuer_override="https://idp.example.com", ) app_no_sls = Application.objects.create( diff --git a/authentik/providers/saml/urls.py b/authentik/providers/saml/urls.py index cf02bd9c48..c56f1e22c2 100644 --- a/authentik/providers/saml/urls.py +++ b/authentik/providers/saml/urls.py @@ -11,6 +11,12 @@ from authentik.providers.saml.views.sp_slo import ( ) urlpatterns = [ + # Base path for Issuer/Entity ID + path( + "/", + sso.SAMLSSOBindingRedirectView.as_view(), + name="base", + ), # SSO Bindings path( "/sso/binding/redirect/", diff --git a/authentik/providers/saml/views/flows.py b/authentik/providers/saml/views/flows.py index 0ee5566f17..aaaf873c21 100644 --- a/authentik/providers/saml/views/flows.py +++ b/authentik/providers/saml/views/flows.py @@ -81,6 +81,7 @@ class SAMLFlowFinalView(ChallengeStageView): "session": auth_session, "name_id": processor.name_id, "name_id_format": processor.name_id_format, + "issuer": processor.issuer, "expires": processor.session_not_on_or_after_datetime, "expiring": True, }, diff --git a/authentik/providers/saml/views/sp_slo.py b/authentik/providers/saml/views/sp_slo.py index 8a8cd3fb77..fe49f68404 100644 --- a/authentik/providers/saml/views/sp_slo.py +++ b/authentik/providers/saml/views/sp_slo.py @@ -107,12 +107,25 @@ class SPInitiatedSLOView(PolicyAccessView): # Store relay state for the logout response plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state + # Look up the session issuer to use in the logout response + auth_session = AuthenticatedSession.from_request(request, request.user) + session_issuer = None + if auth_session: + saml_session = SAMLSession.objects.filter( + session=auth_session, + user=request.user, + provider=self.provider, + ).first() + if saml_session: + session_issuer = saml_session.issuer + if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE: # Native mode - user will be redirected/posted away from authentik processor = LogoutResponseProcessor( self.provider, logout_request, destination=self.provider.sls_url, + issuer=session_issuer, ) if self.provider.sls_binding == SAMLBindings.POST: @@ -152,6 +165,7 @@ class SPInitiatedSLOView(PolicyAccessView): sls_url=self.provider.sls_url, logout_request_id=logout_request.id if logout_request else None, relay_state=relay_state, + issuer=session_issuer, ) LOGGER.debug( @@ -168,6 +182,7 @@ class SPInitiatedSLOView(PolicyAccessView): self.provider, logout_request, destination=self.provider.sls_url, + issuer=session_issuer, ) logout_response = processor.build_response() diff --git a/blueprints/schema.json b/blueprints/schema.json index 0546a7bae4..6a04b567bd 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -10817,11 +10817,10 @@ "title": "Audience", "description": "Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added." }, - "issuer": { + "issuer_override": { "type": "string", - "minLength": 1, - "title": "Issuer", - "description": "Also known as EntityID" + "title": "Issuer override", + "description": "Also known as EntityID. Providing a value overrides the default issuer generated by authentik." }, "assertion_valid_not_before": { "type": "string", diff --git a/packages/client-ts/src/apis/ProvidersApi.ts b/packages/client-ts/src/apis/ProvidersApi.ts index 77fb87a80b..93dc44647e 100644 --- a/packages/client-ts/src/apis/ProvidersApi.ts +++ b/packages/client-ts/src/apis/ProvidersApi.ts @@ -634,7 +634,7 @@ export interface ProvidersSamlListRequest { encryptionKp?: string; invalidationFlow?: string; isBackchannel?: boolean; - issuer?: string; + issuerOverride?: string; logoutMethod?: SAMLLogoutMethods; name?: string; nameIdMapping?: string; @@ -841,7 +841,7 @@ export interface ProvidersWsfedListRequest { encryptionKp?: string; invalidationFlow?: string; isBackchannel?: boolean; - issuer?: string; + issuerOverride?: string; logoutMethod?: SAMLLogoutMethods; name?: string; nameIdMapping?: string; @@ -6842,8 +6842,8 @@ export class ProvidersApi extends runtime.BaseAPI { queryParameters["is_backchannel"] = requestParameters["isBackchannel"]; } - if (requestParameters["issuer"] != null) { - queryParameters["issuer"] = requestParameters["issuer"]; + if (requestParameters["issuerOverride"] != null) { + queryParameters["issuer_override"] = requestParameters["issuerOverride"]; } if (requestParameters["logoutMethod"] != null) { @@ -9326,8 +9326,8 @@ export class ProvidersApi extends runtime.BaseAPI { queryParameters["is_backchannel"] = requestParameters["isBackchannel"]; } - if (requestParameters["issuer"] != null) { - queryParameters["issuer"] = requestParameters["issuer"]; + if (requestParameters["issuerOverride"] != null) { + queryParameters["issuer_override"] = requestParameters["issuerOverride"]; } if (requestParameters["logoutMethod"] != null) { diff --git a/packages/client-ts/src/models/PatchedSAMLProviderRequest.ts b/packages/client-ts/src/models/PatchedSAMLProviderRequest.ts index 1982efad3d..bf9360807e 100644 --- a/packages/client-ts/src/models/PatchedSAMLProviderRequest.ts +++ b/packages/client-ts/src/models/PatchedSAMLProviderRequest.ts @@ -81,11 +81,11 @@ export interface PatchedSAMLProviderRequest { */ audience?: string; /** - * Also known as EntityID + * Also known as EntityID. Providing a value overrides the default issuer generated by authentik. * @type {string} * @memberof PatchedSAMLProviderRequest */ - issuer?: string; + issuerOverride?: string; /** * Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3). * @type {string} @@ -233,7 +233,7 @@ export function PatchedSAMLProviderRequestFromJSONTyped( acsUrl: json["acs_url"] == null ? undefined : json["acs_url"], slsUrl: json["sls_url"] == null ? undefined : json["sls_url"], audience: json["audience"] == null ? undefined : json["audience"], - issuer: json["issuer"] == null ? undefined : json["issuer"], + issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"], assertionValidNotBefore: json["assertion_valid_not_before"] == null ? undefined @@ -306,7 +306,7 @@ export function PatchedSAMLProviderRequestToJSONTyped( acs_url: value["acsUrl"], sls_url: value["slsUrl"], audience: value["audience"], - issuer: value["issuer"], + issuer_override: value["issuerOverride"], assertion_valid_not_before: value["assertionValidNotBefore"], assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"], session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"], diff --git a/packages/client-ts/src/models/SAMLProvider.ts b/packages/client-ts/src/models/SAMLProvider.ts index dbbc5c22d3..7ee4d2af33 100644 --- a/packages/client-ts/src/models/SAMLProvider.ts +++ b/packages/client-ts/src/models/SAMLProvider.ts @@ -135,11 +135,11 @@ export interface SAMLProvider { */ audience?: string; /** - * Also known as EntityID + * Also known as EntityID. Providing a value overrides the default issuer generated by authentik. * @type {string} * @memberof SAMLProvider */ - issuer?: string; + issuerOverride?: string; /** * Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3). * @type {string} @@ -260,6 +260,12 @@ export interface SAMLProvider { * @memberof SAMLProvider */ readonly urlDownloadMetadata: string; + /** + * Get Issuer/EntityID URL + * @type {string} + * @memberof SAMLProvider + */ + readonly urlIssuer: string; /** * Get SSO Post URL * @type {string} @@ -321,6 +327,7 @@ export function instanceOfSAMLProvider(value: object): value is SAMLProvider { if (!("acsUrl" in value) || value["acsUrl"] === undefined) return false; if (!("urlDownloadMetadata" in value) || value["urlDownloadMetadata"] === undefined) return false; + if (!("urlIssuer" in value) || value["urlIssuer"] === undefined) return false; if (!("urlSsoPost" in value) || value["urlSsoPost"] === undefined) return false; if (!("urlSsoRedirect" in value) || value["urlSsoRedirect"] === undefined) return false; if (!("urlSsoInit" in value) || value["urlSsoInit"] === undefined) return false; @@ -356,7 +363,7 @@ export function SAMLProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea acsUrl: json["acs_url"], slsUrl: json["sls_url"] == null ? undefined : json["sls_url"], audience: json["audience"] == null ? undefined : json["audience"], - issuer: json["issuer"] == null ? undefined : json["issuer"], + issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"], assertionValidNotBefore: json["assertion_valid_not_before"] == null ? undefined @@ -406,6 +413,7 @@ export function SAMLProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea ? undefined : SAMLNameIDPolicyEnumFromJSON(json["default_name_id_policy"]), urlDownloadMetadata: json["url_download_metadata"], + urlIssuer: json["url_issuer"], urlSsoPost: json["url_sso_post"], urlSsoRedirect: json["url_sso_redirect"], urlSsoInit: json["url_sso_init"], @@ -431,6 +439,7 @@ export function SAMLProviderToJSONTyped( | "verbose_name_plural" | "meta_model_name" | "url_download_metadata" + | "url_issuer" | "url_sso_post" | "url_sso_redirect" | "url_sso_init" @@ -452,7 +461,7 @@ export function SAMLProviderToJSONTyped( acs_url: value["acsUrl"], sls_url: value["slsUrl"], audience: value["audience"], - issuer: value["issuer"], + issuer_override: value["issuerOverride"], assertion_valid_not_before: value["assertionValidNotBefore"], assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"], session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"], diff --git a/packages/client-ts/src/models/SAMLProviderRequest.ts b/packages/client-ts/src/models/SAMLProviderRequest.ts index fc87c532e5..5d760c33d5 100644 --- a/packages/client-ts/src/models/SAMLProviderRequest.ts +++ b/packages/client-ts/src/models/SAMLProviderRequest.ts @@ -81,11 +81,11 @@ export interface SAMLProviderRequest { */ audience?: string; /** - * Also known as EntityID + * Also known as EntityID. Providing a value overrides the default issuer generated by authentik. * @type {string} * @memberof SAMLProviderRequest */ - issuer?: string; + issuerOverride?: string; /** * Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3). * @type {string} @@ -234,7 +234,7 @@ export function SAMLProviderRequestFromJSONTyped( acsUrl: json["acs_url"], slsUrl: json["sls_url"] == null ? undefined : json["sls_url"], audience: json["audience"] == null ? undefined : json["audience"], - issuer: json["issuer"] == null ? undefined : json["issuer"], + issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"], assertionValidNotBefore: json["assertion_valid_not_before"] == null ? undefined @@ -307,7 +307,7 @@ export function SAMLProviderRequestToJSONTyped( acs_url: value["acsUrl"], sls_url: value["slsUrl"], audience: value["audience"], - issuer: value["issuer"], + issuer_override: value["issuerOverride"], assertion_valid_not_before: value["assertionValidNotBefore"], assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"], session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"], diff --git a/schema.yml b/schema.yml index 0db783cac1..0b0d2b6885 100644 --- a/schema.yml +++ b/schema.yml @@ -18919,7 +18919,7 @@ paths: schema: type: boolean - in: query - name: issuer + name: issuer_override schema: type: string - in: query @@ -20078,7 +20078,7 @@ paths: schema: type: boolean - in: query - name: issuer + name: issuer_override schema: type: string - in: query @@ -50202,10 +50202,10 @@ components: type: string description: Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added. - issuer: + issuer_override: type: string - minLength: 1 - description: Also known as EntityID + description: Also known as EntityID. Providing a value overrides the default + issuer generated by authentik. assertion_valid_not_before: type: string minLength: 1 @@ -53724,9 +53724,10 @@ components: type: string description: Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added. - issuer: + issuer_override: type: string - description: Also known as EntityID + description: Also known as EntityID. Providing a value overrides the default + issuer generated by authentik. assertion_valid_not_before: type: string description: 'Assertion valid not before current time + this value (Format: @@ -53816,6 +53817,10 @@ components: type: string description: Get metadata download URL readOnly: true + url_issuer: + type: string + description: Get Issuer/EntityID URL + readOnly: true url_sso_post: type: string description: Get SSO Post URL @@ -53849,6 +53854,7 @@ components: - name - pk - url_download_metadata + - url_issuer - url_slo_post - url_slo_redirect - url_sso_init @@ -53916,10 +53922,10 @@ components: type: string description: Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added. - issuer: + issuer_override: type: string - minLength: 1 - description: Also known as EntityID + description: Also known as EntityID. Providing a value overrides the default + issuer generated by authentik. assertion_valid_not_before: type: string minLength: 1 diff --git a/tests/e2e/test_provider_saml.py b/tests/e2e/test_provider_saml.py index 74d4c259c5..f1ae945480 100644 --- a/tests/e2e/test_provider_saml.py +++ b/tests/e2e/test_provider_saml.py @@ -39,7 +39,7 @@ class TestProviderSAML(SeleniumTestCase): "9009": "9009", }, environment={ - "SP_ENTITY_ID": provider.issuer, + "SP_ENTITY_ID": provider.issuer_override, "SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "SP_METADATA_URL": metadata_url, **kwargs, @@ -68,7 +68,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), @@ -147,7 +147,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), @@ -226,7 +226,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), @@ -321,7 +321,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), @@ -415,7 +415,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), @@ -503,7 +503,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, signing_kp=create_test_cert(), @@ -553,7 +553,7 @@ class TestProviderSAML(SeleniumTestCase): name=generate_id(), acs_url="http://localhost:9009/saml/acs", audience="authentik-e2e", - issuer="authentik-e2e", + issuer_override="authentik-e2e", sp_binding=SAMLBindings.POST, authorization_flow=authorization_flow, invalidation_flow=invalidation_flow, diff --git a/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts b/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts index 18b6a7ed8e..4f5f19b150 100644 --- a/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts +++ b/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts @@ -42,7 +42,7 @@ const renderSAMLOverview: ProviderOverview = (provider) => { return renderSummary("SAML", provider.name, [ [msg("ACS URL"), provider.acsUrl], [msg("Audience"), provider.audience || "-"], - [msg("Issuer"), provider.issuer], + [msg("Issuer"), provider.urlIssuer], ]); }; diff --git a/web/src/admin/providers/saml/SAMLProviderFormForm.ts b/web/src/admin/providers/saml/SAMLProviderFormForm.ts index 0b5bd502a9..e084a2fd9c 100644 --- a/web/src/admin/providers/saml/SAMLProviderFormForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderFormForm.ts @@ -196,15 +196,6 @@ export function renderForm({ required .errorMessages=${errors.acsUrl} > - +
- ${this.provider.issuer} + ${this.provider.issuerOverride}
@@ -385,7 +385,7 @@ export class SAMLProviderViewPage extends AKElement { class="pf-c-form-control" readonly type="text" - value="${ifDefined(this.provider?.issuer)}" + value="${ifDefined(this.provider?.urlIssuer)}" />