From ef74ca01a24d0607533bf97d8aa844ca80ac279a Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Fri, 6 Feb 2026 00:14:10 +0100 Subject: [PATCH] enterprise/providers: WSFed configurable realm, default wreply (#19996) * enterprise/providers/wsfed: make realm configurable Signed-off-by: Jens Langhammer * make wreply optional, fallback to configure Signed-off-by: Jens Langhammer * use audience instead of issuer Signed-off-by: Jens Langhammer * fix lookup Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .../providers/ws_federation/api/providers.py | 13 +++---------- .../enterprise/providers/ws_federation/models.py | 4 ++++ .../providers/ws_federation/processors/sign_in.py | 11 ++++------- .../providers/ws_federation/processors/sign_out.py | 11 ++++------- .../providers/ws_federation/tests/test_sign_in.py | 2 -- blueprints/schema.json | 5 +++++ schema.yml | 12 +++++++++--- tests/e2e/test_provider_ws_fed.py | 8 +++++++- .../providers/wsfed/WSFederationProviderForm.ts | 8 ++++++++ 9 files changed, 44 insertions(+), 30 deletions(-) diff --git a/authentik/enterprise/providers/ws_federation/api/providers.py b/authentik/enterprise/providers/ws_federation/api/providers.py index 207463b748..9e937343bb 100644 --- a/authentik/enterprise/providers/ws_federation/api/providers.py +++ b/authentik/enterprise/providers/ws_federation/api/providers.py @@ -2,10 +2,9 @@ from django.http import HttpRequest from django.urls import reverse -from rest_framework.fields import SerializerMethodField, URLField +from rest_framework.fields import CharField, SerializerMethodField, URLField from authentik.core.api.providers import ProviderSerializer -from authentik.core.models import Application from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.providers.ws_federation.models import WSFederationProvider from authentik.enterprise.providers.ws_federation.processors.metadata import MetadataProcessor @@ -16,8 +15,8 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial """WSFederationProvider Serializer""" reply_url = URLField(source="acs_url") + wtrealm = CharField(source="audience") url_wsfed = SerializerMethodField() - wtrealm = SerializerMethodField() def get_url_wsfed(self, instance: WSFederationProvider) -> str: """Get WS-Fed url""" @@ -26,16 +25,11 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial request: HttpRequest = self._context["request"]._request return request.build_absolute_uri(reverse("authentik_providers_ws_federation:wsfed")) - def get_wtrealm(self, instance: WSFederationProvider) -> str: - try: - return f"goauthentik.io://app/{instance.application.slug}" - except Application.DoesNotExist: - return None - class Meta(SAMLProviderSerializer.Meta): model = WSFederationProvider fields = ProviderSerializer.Meta.fields + [ "reply_url", + "wtrealm", "assertion_valid_not_before", "assertion_valid_not_on_or_after", "session_valid_not_on_or_after", @@ -51,7 +45,6 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial "default_name_id_policy", "url_download_metadata", "url_wsfed", - "wtrealm", ] extra_kwargs = ProviderSerializer.Meta.extra_kwargs diff --git a/authentik/enterprise/providers/ws_federation/models.py b/authentik/enterprise/providers/ws_federation/models.py index 1194292ff7..d59e52ac5f 100644 --- a/authentik/enterprise/providers/ws_federation/models.py +++ b/authentik/enterprise/providers/ws_federation/models.py @@ -8,6 +8,10 @@ from authentik.providers.saml.models import SAMLProvider class WSFederationProvider(SAMLProvider): """WS-Federation for applications which support WS-Fed.""" + # Alias'd fields: + # - acs_url -> reply_url + # - audience -> realm / wtrealm + @property def serializer(self) -> type[Serializer]: from authentik.enterprise.providers.ws_federation.api.providers import ( diff --git a/authentik/enterprise/providers/ws_federation/processors/sign_in.py b/authentik/enterprise/providers/ws_federation/processors/sign_in.py index ede8c120ff..faf3903859 100644 --- a/authentik/enterprise/providers/ws_federation/processors/sign_in.py +++ b/authentik/enterprise/providers/ws_federation/processors/sign_in.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from urllib.parse import urlparse from django.http import HttpRequest from django.shortcuts import get_object_or_404 @@ -37,8 +36,6 @@ class SignInRequest: wreply: str wctx: str | None - app_slug: str - @staticmethod def parse(request: HttpRequest) -> SignInRequest: action = request.GET.get("wa") @@ -47,26 +44,26 @@ class SignInRequest: realm = request.GET.get("wtrealm") if not realm: raise ValueError("Missing Realm") - parsed = urlparse(realm) req = SignInRequest( wa=action, wtrealm=realm, wreply=request.GET.get("wreply"), wctx=request.GET.get("wctx", ""), - app_slug=parsed.path[1:], ) _, provider = req.get_app_provider() + if not req.wreply: + req.wreply = provider.acs_url if not req.wreply.startswith(provider.acs_url): raise ValueError("Invalid wreply") return req def get_app_provider(self): - application = get_object_or_404(Application, slug=self.app_slug) provider: WSFederationProvider = get_object_or_404( - WSFederationProvider, pk=application.provider_id + WSFederationProvider, audience=self.wtrealm ) + application = get_object_or_404(Application, provider=provider) return application, provider diff --git a/authentik/enterprise/providers/ws_federation/processors/sign_out.py b/authentik/enterprise/providers/ws_federation/processors/sign_out.py index 78b5dd1552..97514b1a5b 100644 --- a/authentik/enterprise/providers/ws_federation/processors/sign_out.py +++ b/authentik/enterprise/providers/ws_federation/processors/sign_out.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from urllib.parse import urlparse from django.http import HttpRequest from django.shortcuts import get_object_or_404 @@ -15,8 +14,6 @@ class SignOutRequest: wtrealm: str wreply: str - app_slug: str - @staticmethod def parse(request: HttpRequest) -> SignOutRequest: action = request.GET.get("wa") @@ -25,23 +22,23 @@ class SignOutRequest: realm = request.GET.get("wtrealm") if not realm: raise ValueError("Missing Realm") - parsed = urlparse(realm) req = SignOutRequest( wa=action, wtrealm=realm, wreply=request.GET.get("wreply"), - app_slug=parsed.path[1:], ) _, provider = req.get_app_provider() + if not req.wreply: + req.wreply = provider.acs_url if not req.wreply.startswith(provider.acs_url): raise ValueError("Invalid wreply") return req def get_app_provider(self): - application = get_object_or_404(Application, slug=self.app_slug) provider: WSFederationProvider = get_object_or_404( - WSFederationProvider, pk=application.provider_id + WSFederationProvider, audience=self.wtrealm ) + application = get_object_or_404(Application, provider=provider) return application, provider diff --git a/authentik/enterprise/providers/ws_federation/tests/test_sign_in.py b/authentik/enterprise/providers/ws_federation/tests/test_sign_in.py index f5bc0092f1..803a9717dd 100644 --- a/authentik/enterprise/providers/ws_federation/tests/test_sign_in.py +++ b/authentik/enterprise/providers/ws_federation/tests/test_sign_in.py @@ -43,7 +43,6 @@ class TestWSFedSignIn(TestCase): wtrealm="", wreply="", wctx=None, - app_slug="", ), ) token = proc.response()[WS_FED_POST_KEY_RESULT] @@ -65,7 +64,6 @@ class TestWSFedSignIn(TestCase): wtrealm="", wreply="", wctx=None, - app_slug="", ), ) token = proc.response()[WS_FED_POST_KEY_RESULT] diff --git a/blueprints/schema.json b/blueprints/schema.json index 2ff2260229..15b60f9b45 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7165,6 +7165,11 @@ "minLength": 1, "title": "Reply url" }, + "wtrealm": { + "type": "string", + "minLength": 1, + "title": "Wtrealm" + }, "assertion_valid_not_before": { "type": "string", "minLength": 1, diff --git a/schema.yml b/schema.yml index c87cb25e1b..04afb270e1 100644 --- a/schema.yml +++ b/schema.yml @@ -50218,6 +50218,9 @@ components: type: string format: uri minLength: 1 + wtrealm: + type: string + minLength: 1 assertion_valid_not_before: type: string minLength: 1 @@ -57127,6 +57130,8 @@ components: reply_url: type: string format: uri + wtrealm: + type: string assertion_valid_not_before: type: string description: 'Assertion valid not before current time + this value (Format: @@ -57187,9 +57192,6 @@ components: type: string description: Get WS-Fed url readOnly: true - wtrealm: - type: string - readOnly: true required: - assigned_application_name - assigned_application_slug @@ -57237,6 +57239,9 @@ components: type: string format: uri minLength: 1 + wtrealm: + type: string + minLength: 1 assertion_valid_not_before: type: string minLength: 1 @@ -57297,6 +57302,7 @@ components: - invalidation_flow - name - reply_url + - wtrealm WebAuthnDevice: type: object description: Serializer for WebAuthn authenticator devices diff --git a/tests/e2e/test_provider_ws_fed.py b/tests/e2e/test_provider_ws_fed.py index 5f0cd99000..1b5f686b45 100644 --- a/tests/e2e/test_provider_ws_fed.py +++ b/tests/e2e/test_provider_ws_fed.py @@ -18,6 +18,10 @@ from tests.e2e.utils import SeleniumTestCase, retry class TestProviderWSFed(SeleniumTestCase): """test WS Federation flow""" + def setUp(self): + self.realm = generate_id() + super().setUp() + def setup_client(self, provider: WSFederationProvider, app: Application, **kwargs): metadata_url = ( self.url( @@ -32,7 +36,7 @@ class TestProviderWSFed(SeleniumTestCase): "8080": "8080", }, environment={ - "WSFED_TEST_SP_WTREALM": f"goauthentik.io://app/{app.slug}", + "WSFED_TEST_SP_WTREALM": self.realm, "WSFED_TEST_SP_METADATA": metadata_url, **kwargs, }, @@ -61,6 +65,7 @@ class TestProviderWSFed(SeleniumTestCase): provider = WSFederationProvider.objects.create( name=generate_id(), acs_url="http://localhost:8080", + audience=self.realm, authorization_flow=authorization_flow, invalidation_flow=invalidation_flow, signing_kp=create_test_cert(), @@ -147,6 +152,7 @@ class TestProviderWSFed(SeleniumTestCase): provider = WSFederationProvider.objects.create( name=generate_id(), acs_url="http://localhost:8080", + audience=self.realm, authorization_flow=authorization_flow, signing_kp=create_test_cert(), ) diff --git a/web/src/admin/providers/wsfed/WSFederationProviderForm.ts b/web/src/admin/providers/wsfed/WSFederationProviderForm.ts index 974b04a13f..9d3d0c05b9 100644 --- a/web/src/admin/providers/wsfed/WSFederationProviderForm.ts +++ b/web/src/admin/providers/wsfed/WSFederationProviderForm.ts @@ -107,6 +107,14 @@ export class WSFederationProviderForm extends BaseProviderForm +