enterprise/providers: WSFed configurable realm, default wreply (#19996)

* enterprise/providers/wsfed: make realm configurable

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make wreply optional, fallback to configure

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use audience instead of issuer

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lookup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-02-06 00:14:10 +01:00
committed by GitHub
parent fd778b18ad
commit ef74ca01a2
9 changed files with 44 additions and 30 deletions
@@ -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
@@ -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 (
@@ -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
@@ -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
@@ -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]
+5
View File
@@ -7165,6 +7165,11 @@
"minLength": 1,
"title": "Reply url"
},
"wtrealm": {
"type": "string",
"minLength": 1,
"title": "Wtrealm"
},
"assertion_valid_not_before": {
"type": "string",
"minLength": 1,
+9 -3
View File
@@ -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
+7 -1
View File
@@ -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(),
)
@@ -107,6 +107,14 @@ export class WSFederationProviderForm extends BaseProviderForm<WSFederationProvi
value="${ifDefined(this.instance?.replyUrl)}"
required
></ak-text-input>
<ak-text-input
name="wtrealm"
label=${msg("Realm")}
placeholder=${msg("")}
input-hint="code"
value="${ifDefined(this.instance?.wtrealm)}"
required
></ak-text-input>
</div>
</ak-form-group>