enterprise/providers/ws_fed: add SAML 1.1 support (#23851)

* enterprise/providers/ws_fed: add SAML 1.1 support

thanks microsoft

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

* fix session_index

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

* fix issuer

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

* show issuer url in ui too

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

* app slug in URL to lookup realm when not in parameters

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

* broader cleanup

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

* fix tests

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

* sigh

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

* sigh nr 2

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-07-23 12:52:42 +01:00
committed by GitHub
parent 539da39b01
commit cd18cf3a5c
22 changed files with 900 additions and 46 deletions

View File

@@ -4,6 +4,7 @@ from django.http import HttpRequest
from django.urls import reverse
from rest_framework.fields import CharField, SerializerMethodField, URLField
from authentik.common.saml.constants import DEFAULT_ISSUER
from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Provider
from authentik.enterprise.api import EnterpriseRequiredMixin
@@ -18,6 +19,7 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial
reply_url = URLField(source="acs_url")
wtrealm = CharField(source="audience")
url_wsfed = SerializerMethodField()
url_issuer = SerializerMethodField()
def get_url_download_metadata(self, instance: WSFederationProvider) -> str:
"""Get metadata download URL"""
@@ -47,7 +49,32 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial
if "request" not in self._context:
return ""
request: HttpRequest = self._context["request"]._request
return request.build_absolute_uri(reverse("authentik_providers_ws_federation:wsfed"))
try:
return request.build_absolute_uri(
reverse(
"authentik_providers_ws_federation:wsfed-app-specific",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return ""
def get_url_issuer(self, instance: WSFederationProvider) -> 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_ws_federation:metadata-download",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return DEFAULT_ISSUER
class Meta(SAMLProviderSerializer.Meta):
model = WSFederationProvider
@@ -60,6 +87,7 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial
"property_mappings",
"name_id_mapping",
"authn_context_class_ref_mapping",
"saml_version",
"digest_algorithm",
"signature_algorithm",
"signing_kp",
@@ -69,6 +97,7 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial
"default_name_id_policy",
"url_download_metadata",
"url_wsfed",
"url_issuer",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.15 on 2026-07-08 12:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_ws_federation", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="wsfederationprovider",
name="saml_version",
field=models.TextField(
choices=[("1.1", "SAML 1.1"), ("2.0", "SAML 2.0")],
default="2.0",
help_text="SAML assertion version to issue in the security token. Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1.",
),
),
]

View File

@@ -1,3 +1,4 @@
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
@@ -5,6 +6,13 @@ from rest_framework.serializers import Serializer
from authentik.providers.saml.models import SAMLProvider
class WSFederationSAMLVersion(models.TextChoices):
"""SAML Assertion version issued by a WS-Federation provider"""
SAML_1_1 = "1.1", _("SAML 1.1")
SAML_2_0 = "2.0", _("SAML 2.0")
class WSFederationProvider(SAMLProvider):
"""WS-Federation for applications which support WS-Fed."""
@@ -12,6 +20,15 @@ class WSFederationProvider(SAMLProvider):
# - acs_url -> reply_url
# - audience -> realm / wtrealm
saml_version = models.TextField(
choices=WSFederationSAMLVersion.choices,
default=WSFederationSAMLVersion.SAML_2_0,
help_text=_(
"SAML assertion version to issue in the security token. Microsoft Entra ID and "
"classic ADFS-style relying parties typically require SAML 1.1."
),
)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.ws_federation.api.providers import (

View File

@@ -0,0 +1,218 @@
"""SAML 1.1 Assertion generator for WS-Federation"""
from hashlib import sha256
from types import GeneratorType
import xmlsec
from lxml.etree import Element, SubElement, _Element # nosec
from structlog.stdlib import get_logger
from authentik.common.saml.constants import (
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_SIGNATURE,
SIGN_ALGORITHM_TRANSFORM_MAP,
)
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event
from authentik.lib.xml import remove_xml_newlines
from authentik.providers.saml.models import SAMLPropertyMapping
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.sources.saml.exceptions import InvalidSignature
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD
LOGGER = get_logger()
# SAML 1.0 and 1.1 share this namespace; version is set via MajorVersion/MinorVersion attributes
NS_SAML11_ASSERTION = "urn:oasis:names:tc:SAML:1.0:assertion"
NS_MAP_SAML11 = {"saml": NS_SAML11_ASSERTION, "ds": NS_SIGNATURE}
SAML11_CM_BEARER = "urn:oasis:names:tc:SAML:1.0:cm:bearer"
SAML11_AM_PASSWORD = "urn:oasis:names:tc:SAML:1.0:am:password"
SAML11_AM_UNSPECIFIED = "urn:oasis:names:tc:SAML:1.0:am:unspecified"
WSFED_ATTRIBUTE_NAMESPACE = "http://schemas.xmlsoap.org/claims"
class SAML11AssertionProcessor(AssertionProcessor):
"""SAML 1.1 assertion builder, overriding the SAML 2.0 methods that differ."""
def get_name_id(self) -> _Element:
# same value/format resolution as SAML 2.0, wrapped as NameIdentifier instead of NameID
name_id = super().get_name_id()
name_identifier = Element(f"{{{NS_SAML11_ASSERTION}}}NameIdentifier")
name_identifier.attrib["Format"] = name_id.attrib["Format"]
name_identifier.text = name_id.text
return name_identifier
def get_assertion_subject(self) -> _Element:
# SAML 1.1 has no assertion-level Subject; each statement embeds its own
subject = Element(f"{{{NS_SAML11_ASSERTION}}}Subject")
subject.append(self.get_name_id())
subject_confirmation = SubElement(subject, f"{{{NS_SAML11_ASSERTION}}}SubjectConfirmation")
confirmation_method = SubElement(
subject_confirmation, f"{{{NS_SAML11_ASSERTION}}}ConfirmationMethod"
)
confirmation_method.text = SAML11_CM_BEARER
return subject
def get_assertion_conditions(self) -> _Element:
conditions = Element(f"{{{NS_SAML11_ASSERTION}}}Conditions")
conditions.attrib["NotBefore"] = self._valid_not_before
conditions.attrib["NotOnOrAfter"] = self._valid_not_on_or_after
if self.provider.audience != "":
audience_restriction_condition = SubElement(
conditions, f"{{{NS_SAML11_ASSERTION}}}AudienceRestrictionCondition"
)
audience = SubElement(
audience_restriction_condition, f"{{{NS_SAML11_ASSERTION}}}Audience"
)
audience.text = self.provider.audience
return conditions
def get_assertion_auth_n_statement(self) -> _Element:
auth_statement = Element(f"{{{NS_SAML11_ASSERTION}}}AuthenticationStatement")
auth_statement.attrib["AuthenticationInstant"] = self._auth_instant
auth_statement.attrib["AuthenticationMethod"] = SAML11_AM_UNSPECIFIED
self.session_index = sha256(
self.http_request.session.session_key.encode("ascii")
).hexdigest()
event = get_login_event(self.http_request)
if event and event.context.get(PLAN_CONTEXT_METHOD, "") == "password":
auth_statement.attrib["AuthenticationMethod"] = SAML11_AM_PASSWORD
if self.provider.authn_context_class_ref_mapping:
try:
value = self.provider.authn_context_class_ref_mapping.evaluate(
user=self.http_request.user,
request=self.http_request,
provider=self.provider,
)
if value is not None:
auth_statement.attrib["AuthenticationMethod"] = str(value)
except PropertyMappingExpressionException as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(
"Failed to evaluate property-mapping: "
f"'{self.provider.authn_context_class_ref_mapping.name}'"
),
provider=self.provider,
mapping=self.provider.authn_context_class_ref_mapping,
).from_http(self.http_request)
LOGGER.warning("Failed to evaluate property mapping", exc=exc)
auth_statement.append(self.get_assertion_subject())
return auth_statement
def get_attributes(self) -> _Element | None:
# None if empty: SAML 1.1's schema requires at least one Attribute per AttributeStatement
attribute_statement = Element(f"{{{NS_SAML11_ASSERTION}}}AttributeStatement")
attribute_statement.append(self.get_assertion_subject())
user = self.http_request.user
has_attribute = False
for mapping in SAMLPropertyMapping.objects.filter(provider=self.provider).order_by(
"saml_name"
):
try:
mapping: SAMLPropertyMapping
value = mapping.evaluate(
user=user,
request=self.http_request,
provider=self.provider,
)
if value is None:
continue
attribute = Element(f"{{{NS_SAML11_ASSERTION}}}Attribute")
attribute.attrib["AttributeName"] = mapping.saml_name
attribute.attrib["AttributeNamespace"] = WSFED_ATTRIBUTE_NAMESPACE
if not isinstance(value, list | GeneratorType):
value = [value]
for value_item in value:
attribute_value = SubElement(
attribute, f"{{{NS_SAML11_ASSERTION}}}AttributeValue"
)
str_value = str(value_item) if not isinstance(value_item, str) else value_item
attribute_value.text = str_value
attribute_statement.append(attribute)
has_attribute = True
except (PropertyMappingExpressionException, ValueError) as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping: '{mapping.name}'",
provider=self.provider,
mapping=mapping,
).from_http(self.http_request)
LOGGER.warning("Failed to evaluate property mapping", exc=exc)
continue
if not has_attribute:
return None
return attribute_statement
def get_assertion(self) -> _Element:
assertion = Element(f"{{{NS_SAML11_ASSERTION}}}Assertion", nsmap=NS_MAP_SAML11)
assertion.attrib["MajorVersion"] = "1"
assertion.attrib["MinorVersion"] = "1"
assertion.attrib["AssertionID"] = self._assertion_id
assertion.attrib["IssueInstant"] = self._issue_instant
self.issuer = self._get_issuer_value()
assertion.attrib["Issuer"] = self.issuer
assertion.append(self.get_assertion_conditions())
assertion.append(self.get_assertion_auth_n_statement())
attribute_statement = self.get_attributes()
if attribute_statement is not None:
assertion.append(attribute_statement)
# unlike SAML 2.0, ds:Signature must be the last child of Assertion
if self.provider.signing_kp and self.provider.sign_assertion:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
assertion,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
assertion.append(signature)
return assertion
def _sign(self, element: _Element):
# same as AssertionProcessor._sign, but referencing AssertionID instead of ID
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
self.provider.digest_algorithm, xmlsec.constants.TransformSha1
)
xmlsec.tree.add_ids(element, ["AssertionID"])
signature_node = xmlsec.tree.find_node(element, xmlsec.constants.NodeSignature)
ref = xmlsec.template.add_reference(
signature_node,
digest_algorithm_transform,
uri="#" + element.attrib["AssertionID"],
)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
key_info = xmlsec.template.ensure_key_info(signature_node)
xmlsec.template.add_x509_data(key_info)
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data,
xmlsec.constants.KeyDataFormatPem,
None,
)
key.load_cert_from_memory(
self.provider.signing_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
)
ctx.key = key
try:
ctx.sign(remove_xml_newlines(element, signature_node))
except xmlsec.Error as exc:
raise InvalidSignature() from exc

View File

@@ -1,9 +1,13 @@
from authentik.common.saml.constants import NS_MAP as _map
from authentik.enterprise.providers.ws_federation.models import WSFederationSAMLVersion
WS_FED_ACTION_SIGN_IN = "wsignin1.0"
WS_FED_ACTION_SIGN_OUT = "wsignout1.0"
WS_FED_ACTION_SIGN_OUT_CLEANUP = "wsignoutcleanup1.0"
WS_FED_QS_ACTION = "wa"
WS_FED_QS_REALM = "wtrealm"
WS_FED_QS_REPLY = "wreply"
WS_FED_POST_KEY_ACTION = "wa"
WS_FED_POST_KEY_RESULT = "wresult"
WS_FED_POST_KEY_CONTEXT = "wctx"
@@ -11,10 +15,18 @@ WS_FED_POST_KEY_CONTEXT = "wctx"
WSS_TOKEN_TYPE_SAML2 = (
"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" # nosec
)
WSS_TOKEN_TYPE_SAML11 = (
"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" # nosec
)
WSS_KEY_IDENTIFIER_SAML_ID = (
"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID"
)
WSS_TOKEN_TYPE_BY_VERSION = {
WSFederationSAMLVersion.SAML_1_1: WSS_TOKEN_TYPE_SAML11,
WSFederationSAMLVersion.SAML_2_0: WSS_TOKEN_TYPE_SAML2,
}
NS_WS_FED_PROTOCOL = "http://docs.oasis-open.org/wsfed/federation/200706"
NS_WS_FED_TRUST = "http://schemas.xmlsoap.org/ws/2005/02/trust"
NS_WSI = "http://www.w3.org/2001/XMLSchema-instance"

View File

@@ -22,7 +22,12 @@ class MetadataProcessor(BaseMetadataProcessor):
address = SubElement(endpoint_ref, f"{{{NS_ADDRESSING}}}Address", nsmap=NS_MAP)
address.text = self.http_request.build_absolute_uri(
reverse("authentik_providers_ws_federation:wsfed")
reverse(
"authentik_providers_ws_federation:wsfed-app-specific",
kwargs={
"application_slug": self.provider.application.slug,
},
)
)
def add_role_descriptor_sts(self, entity_descriptor: _Element):

View File

@@ -7,7 +7,13 @@ from lxml import etree # nosec
from lxml.etree import Element, SubElement, _Element # nosec
from authentik.core.models import Application
from authentik.enterprise.providers.ws_federation.models import WSFederationProvider
from authentik.enterprise.providers.ws_federation.models import (
WSFederationProvider,
WSFederationSAMLVersion,
)
from authentik.enterprise.providers.ws_federation.processors.assertion_saml11 import (
SAML11AssertionProcessor,
)
from authentik.enterprise.providers.ws_federation.processors.constants import (
NS_ADDRESSING,
NS_MAP,
@@ -20,8 +26,11 @@ from authentik.enterprise.providers.ws_federation.processors.constants import (
WS_FED_POST_KEY_ACTION,
WS_FED_POST_KEY_CONTEXT,
WS_FED_POST_KEY_RESULT,
WS_FED_QS_ACTION,
WS_FED_QS_REALM,
WS_FED_QS_REPLY,
WSS_KEY_IDENTIFIER_SAML_ID,
WSS_TOKEN_TYPE_SAML2,
WSS_TOKEN_TYPE_BY_VERSION,
)
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.utils import delete_none_values
@@ -39,28 +48,26 @@ class SignInRequest:
@staticmethod
def parse(request: HttpRequest) -> SignInRequest:
action = request.GET.get("wa")
if action != WS_FED_ACTION_SIGN_IN:
raise ValueError("Invalid action")
realm = request.GET.get("wtrealm")
if not realm:
raise ValueError("Missing Realm")
req = SignInRequest(
wa=action,
wtrealm=realm,
wreply=request.GET.get("wreply"),
wctx=request.GET.get("wctx", ""),
wa=request.GET.get(WS_FED_QS_ACTION),
wtrealm=request.GET.get(WS_FED_QS_REALM),
wreply=request.GET.get(WS_FED_QS_REPLY),
wctx=request.GET.get(WS_FED_POST_KEY_CONTEXT, ""),
)
return req
_, provider = req.get_app_provider()
if not req.wreply:
req.wreply = provider.acs_url
reply = urlparse(req.wreply)
def __post_init__(self):
if self.wa != WS_FED_ACTION_SIGN_IN:
raise ValueError("Invalid action")
if not self.wtrealm:
raise ValueError("Missing Realm")
_, provider = self.get_app_provider()
if not self.wreply:
self.wreply = provider.acs_url
reply = urlparse(self.wreply)
configured = urlparse(provider.acs_url)
if not (reply[:2] == configured[:2] and reply.path.startswith(configured.path)):
raise ValueError("Invalid wreply")
return req
def get_app_provider(self):
provider: WSFederationProvider = get_object_or_404(
@@ -82,7 +89,12 @@ class SignInProcessor:
self.provider = provider
self.request = request
self.sign_in_request = sign_in_request
self.saml_processor = AssertionProcessor(self.provider, self.request, AuthNRequest())
processor_cls = (
SAML11AssertionProcessor
if self.provider.saml_version == WSFederationSAMLVersion.SAML_1_1
else AssertionProcessor
)
self.saml_processor = processor_cls(self.provider, self.request, AuthNRequest())
self.saml_processor.provider.audience = self.sign_in_request.wtrealm
if self.provider.signing_kp:
self.saml_processor.provider.sign_assertion = True
@@ -105,7 +117,7 @@ class SignInProcessor:
)
token_type = SubElement(root, f"{{{NS_WS_FED_TRUST}}}TokenType")
token_type.text = WSS_TOKEN_TYPE_SAML2
token_type.text = WSS_TOKEN_TYPE_BY_VERSION[self.provider.saml_version]
request_type = SubElement(root, f"{{{NS_WS_FED_TRUST}}}RequestType")
request_type.text = "http://schemas.xmlsoap.org/ws/2005/02/trust/Issue"
@@ -143,7 +155,9 @@ class SignInProcessor:
def response_add_attached_reference(self, tag: str, value: str) -> _Element:
ref = Element(f"{{{NS_WS_FED_TRUST}}}{tag}")
sec_token_ref = SubElement(ref, f"{{{NS_WSS_SEC}}}SecurityTokenReference")
sec_token_ref.attrib[f"{{{NS_WSS_D3P1}}}TokenType"] = WSS_TOKEN_TYPE_SAML2
sec_token_ref.attrib[f"{{{NS_WSS_D3P1}}}TokenType"] = WSS_TOKEN_TYPE_BY_VERSION[
self.provider.saml_version
]
key_identifier = SubElement(sec_token_ref, f"{{{NS_WSS_SEC}}}KeyIdentifier")
key_identifier.attrib["ValueType"] = WSS_KEY_IDENTIFIER_SAML_ID
@@ -152,7 +166,8 @@ class SignInProcessor:
def response(self) -> dict[str, str]:
root = self.create_response_token()
assertion = root.xpath("//saml:Assertion", namespaces=NS_MAP)[0]
# match by local name, since "saml" may be bound to the 1.1 or 2.0 namespace here
assertion = root.xpath("//*[local-name()='Assertion']")[0]
if self.provider.signing_kp:
self.saml_processor._sign(assertion)
str_token = etree.tostring(root).decode("utf-8") # nosec

View File

@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import InitVar, dataclass
from urllib.parse import urlparse
from django.http import HttpRequest
@@ -6,7 +6,12 @@ from django.shortcuts import get_object_or_404
from authentik.core.models import Application
from authentik.enterprise.providers.ws_federation.models import WSFederationProvider
from authentik.enterprise.providers.ws_federation.processors.constants import WS_FED_ACTION_SIGN_OUT
from authentik.enterprise.providers.ws_federation.processors.constants import (
WS_FED_ACTION_SIGN_OUT,
WS_FED_QS_ACTION,
WS_FED_QS_REALM,
WS_FED_QS_REPLY,
)
@dataclass()
@@ -14,30 +19,38 @@ class SignOutRequest:
wa: str
wtrealm: str
wreply: str
request: InitVar[HttpRequest]
@staticmethod
def parse(request: HttpRequest) -> SignOutRequest:
action = request.GET.get("wa")
if action != WS_FED_ACTION_SIGN_OUT:
raise ValueError("Invalid action")
realm = request.GET.get("wtrealm")
if not realm:
raise ValueError("Missing Realm")
req = SignOutRequest(
wa=action,
wtrealm=realm,
wreply=request.GET.get("wreply"),
return SignOutRequest(
wa=request.GET.get(WS_FED_QS_ACTION),
wtrealm=request.GET.get(WS_FED_QS_REALM),
wreply=request.GET.get(WS_FED_QS_REPLY),
request=request,
)
_, provider = req.get_app_provider()
if not req.wreply:
req.wreply = provider.acs_url
reply = urlparse(req.wreply)
def __post_init__(self, request: HttpRequest):
if self.wa != WS_FED_ACTION_SIGN_OUT:
raise ValueError("Invalid action")
self.__post_init_resolve_realm(request)
_, provider = self.get_app_provider()
if not self.wreply:
self.wreply = provider.acs_url
if not self.wtrealm:
raise ValueError("Missing Realm")
reply = urlparse(self.wreply)
configured = urlparse(provider.acs_url)
if not (reply[:2] == configured[:2] and reply.path.startswith(configured.path)):
raise ValueError("Invalid wreply")
return req
def __post_init_resolve_realm(self, request: HttpRequest):
slug = request.resolver_match.kwargs.get("application_slug")
if not slug:
return
app = get_object_or_404(Application, slug=slug)
provider = get_object_or_404(WSFederationProvider, pk=app.provider_id)
self.wtrealm = provider.audience
def get_app_provider(self):
provider: WSFederationProvider = get_object_or_404(

View File

@@ -5,11 +5,18 @@ from lxml import etree # nosec
from authentik.core.models import Application
from authentik.core.tests.utils import RequestFactory, create_test_cert, create_test_flow
from authentik.enterprise.providers.ws_federation.models import WSFederationProvider
from authentik.enterprise.providers.ws_federation.models import (
WSFederationProvider,
WSFederationSAMLVersion,
)
from authentik.enterprise.providers.ws_federation.processors.assertion_saml11 import (
NS_SAML11_ASSERTION,
)
from authentik.enterprise.providers.ws_federation.processors.constants import (
NS_MAP,
WS_FED_ACTION_SIGN_IN,
WS_FED_POST_KEY_RESULT,
WSS_TOKEN_TYPE_SAML11,
)
from authentik.enterprise.providers.ws_federation.processors.sign_in import (
SignInProcessor,
@@ -17,6 +24,7 @@ from authentik.enterprise.providers.ws_federation.processors.sign_in import (
)
from authentik.lib.generators import generate_id
from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLPropertyMapping
class TestWSFedSignIn(TestCase):
@@ -55,7 +63,7 @@ class TestWSFedSignIn(TestCase):
request,
SignInRequest(
wa=WS_FED_ACTION_SIGN_IN,
wtrealm="",
wtrealm=self.provider.audience,
wreply="",
wctx=None,
),
@@ -76,7 +84,7 @@ class TestWSFedSignIn(TestCase):
request,
SignInRequest(
wa=WS_FED_ACTION_SIGN_IN,
wtrealm="",
wtrealm=self.provider.audience,
wreply="",
wctx=None,
),
@@ -96,3 +104,110 @@ class TestWSFedSignIn(TestCase):
None,
)
ctx.verify(signature_node)
class TestWSFedSignInSAML11(TestCase):
# NS_MAP binds the "saml" prefix to the SAML 2.0 namespace; override it for SAML 1.1 lookups
ns_map = {**NS_MAP, "saml": NS_SAML11_ASSERTION}
def setUp(self):
self.flow = create_test_flow()
self.cert = create_test_cert()
self.provider = WSFederationProvider.objects.create(
name=generate_id(),
authorization_flow=self.flow,
signing_kp=self.cert,
acs_url="https://t.goauthentik.io",
audience="foo",
saml_version=WSFederationSAMLVersion.SAML_1_1,
)
self.app = Application.objects.create(
name=generate_id(), slug=generate_id(), provider=self.provider
)
self.factory = RequestFactory()
def _get_token(self) -> str:
request = self.factory.get("/", user=get_anonymous_user())
proc = SignInProcessor(
self.provider,
request,
SignInRequest(
wa=WS_FED_ACTION_SIGN_IN,
wtrealm=self.provider.audience,
wreply="",
wctx=None,
),
)
return proc.response()[WS_FED_POST_KEY_RESULT]
def test_token_gen(self):
token = self._get_token()
root = lxml_from_string(token)
schema = etree.XMLSchema(
etree.parse(source="schemas/ws-trust.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(etree=root), schema.error_log)
assertion = root.xpath("//*[local-name()='Assertion']")[0]
self.assertEqual(assertion.tag, f"{{{NS_SAML11_ASSERTION}}}Assertion")
self.assertEqual(assertion.attrib["MajorVersion"], "1")
self.assertEqual(assertion.attrib["MinorVersion"], "1")
self.assertIn("AssertionID", assertion.attrib)
self.assertNotIn("ID", assertion.attrib)
assertion_schema = etree.XMLSchema(
etree.parse( # nosec
source="schemas/oasis-sstc-saml-schema-assertion-1.1.xsd",
parser=etree.XMLParser(),
)
)
self.assertTrue(assertion_schema.validate(etree=assertion), assertion_schema.error_log)
token_type = root.xpath("//t:TokenType", namespaces=self.ns_map)[0]
self.assertEqual(token_type.text, WSS_TOKEN_TYPE_SAML11)
# SAML 1.1 uses NameIdentifier/AuthenticationStatement, not NameID/AuthnStatement
self.assertEqual(len(assertion.xpath("//saml:NameIdentifier", namespaces=self.ns_map)), 1)
self.assertEqual(
len(assertion.xpath("//saml:AuthenticationStatement", namespaces=self.ns_map)), 1
)
def test_signature(self):
token = self._get_token()
root = lxml_from_string(token)
xmlsec.tree.add_ids(root, ["AssertionID"])
signature_nodes = root.xpath(
"//*[local-name()='Assertion']/ds:Signature", namespaces=self.ns_map
)
self.assertEqual(len(signature_nodes), 1)
signature_node = signature_nodes[0]
ctx = xmlsec.SignatureContext()
ctx.key = xmlsec.Key.from_memory(
self.cert.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
None,
)
ctx.verify(signature_node)
def test_attribute_statement(self):
mapping = SAMLPropertyMapping.objects.create(
name=generate_id(), saml_name="test-claim", expression="return 'test-value'"
)
self.provider.property_mappings.add(mapping)
token = self._get_token()
root = lxml_from_string(token)
attributes = root.xpath("//saml:Attribute", namespaces=self.ns_map)
self.assertEqual(len(attributes), 1)
attribute = attributes[0]
self.assertEqual(attribute.attrib["AttributeName"], "test-claim")
self.assertIn("AttributeNamespace", attribute.attrib)
self.assertNotIn("Name", attribute.attrib)
self.assertNotIn("FriendlyName", attribute.attrib)
values = attribute.xpath("saml:AttributeValue", namespaces=self.ns_map)
self.assertEqual(len(values), 1)
self.assertEqual(values[0].text, "test-value")

View File

@@ -11,6 +11,11 @@ urlpatterns = [
WSFedEntryView.as_view(),
name="wsfed",
),
path(
"<slug:application_slug>/",
WSFedEntryView.as_view(),
name="wsfed-app-specific",
),
# Metadata
path(
"<slug:application_slug>/metadata/",

View File

@@ -7928,6 +7928,15 @@
"title": "AuthnContextClassRef Property Mapping",
"description": "Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate."
},
"saml_version": {
"type": "string",
"enum": [
"1.1",
"2.0"
],
"title": "Saml version",
"description": "SAML assertion version to issue in the security token. Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1."
},
"digest_algorithm": {
"type": "string",
"enum": [

View File

@@ -214,6 +214,7 @@ import { type SAMLMetadata, SAMLMetadataFromJSON } from "../models/SAMLMetadata"
import { type SAMLNameIDPolicyEnum } from "../models/SAMLNameIDPolicyEnum";
import { type SAMLProvider, SAMLProviderFromJSON } from "../models/SAMLProvider";
import { type SAMLProviderRequest, SAMLProviderRequestToJSON } from "../models/SAMLProviderRequest";
import { type SamlVersionEnum } from "../models/SamlVersionEnum";
import { type SCIMProvider, SCIMProviderFromJSON } from "../models/SCIMProvider";
import { type SCIMProviderGroup, SCIMProviderGroupFromJSON } from "../models/SCIMProviderGroup";
import {
@@ -923,6 +924,7 @@ export interface ProvidersWsfedListRequest {
page?: number;
pageSize?: number;
propertyMappings?: Array<string>;
samlVersion?: SamlVersionEnum;
search?: string;
sessionValidNotOnOrAfter?: string;
signAssertion?: boolean;
@@ -9432,6 +9434,10 @@ export class ProvidersApi extends runtime.BaseAPI {
queryParameters["property_mappings"] = requestParameters["propertyMappings"];
}
if (requestParameters["samlVersion"] != null) {
queryParameters["saml_version"] = requestParameters["samlVersion"];
}
if (requestParameters["search"] != null) {
queryParameters["search"] = requestParameters["search"];
}

View File

@@ -16,6 +16,8 @@ import type { DigestAlgorithmEnum } from "./DigestAlgorithmEnum";
import { DigestAlgorithmEnumFromJSON, DigestAlgorithmEnumToJSON } from "./DigestAlgorithmEnum";
import type { SAMLNameIDPolicyEnum } from "./SAMLNameIDPolicyEnum";
import { SAMLNameIDPolicyEnumFromJSON, SAMLNameIDPolicyEnumToJSON } from "./SAMLNameIDPolicyEnum";
import type { SamlVersionEnum } from "./SamlVersionEnum";
import { SamlVersionEnumFromJSON, SamlVersionEnumToJSON } from "./SamlVersionEnum";
import type { SignatureAlgorithmEnum } from "./SignatureAlgorithmEnum";
import {
SignatureAlgorithmEnumFromJSON,
@@ -100,6 +102,12 @@ export interface PatchedWSFederationProviderRequest {
* @memberof PatchedWSFederationProviderRequest
*/
authnContextClassRefMapping?: string | null;
/**
* SAML assertion version to issue in the security token. Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1.
* @type {SamlVersionEnum}
* @memberof PatchedWSFederationProviderRequest
*/
samlVersion?: SamlVersionEnum;
/**
*
* @type {DigestAlgorithmEnum}
@@ -193,6 +201,10 @@ export function PatchedWSFederationProviderRequestFromJSONTyped(
json["authn_context_class_ref_mapping"] == null
? undefined
: json["authn_context_class_ref_mapping"],
samlVersion:
json["saml_version"] == null
? undefined
: SamlVersionEnumFromJSON(json["saml_version"]),
digestAlgorithm:
json["digest_algorithm"] == null
? undefined
@@ -240,6 +252,7 @@ export function PatchedWSFederationProviderRequestToJSONTyped(
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],
name_id_mapping: value["nameIdMapping"],
authn_context_class_ref_mapping: value["authnContextClassRefMapping"],
saml_version: SamlVersionEnumToJSON(value["samlVersion"]),
digest_algorithm: DigestAlgorithmEnumToJSON(value["digestAlgorithm"]),
signature_algorithm: SignatureAlgorithmEnumToJSON(value["signatureAlgorithm"]),
signing_kp: value["signingKp"],

View File

@@ -0,0 +1,57 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.8.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const SamlVersionEnum = {
_11: "1.1",
_20: "2.0",
UnknownDefaultOpenApi: "11184809",
} as const;
export type SamlVersionEnum = (typeof SamlVersionEnum)[keyof typeof SamlVersionEnum];
export function instanceOfSamlVersionEnum(value: any): boolean {
for (const key in SamlVersionEnum) {
if (Object.prototype.hasOwnProperty.call(SamlVersionEnum, key)) {
if (SamlVersionEnum[key as keyof typeof SamlVersionEnum] === value) {
return true;
}
}
}
return false;
}
export function SamlVersionEnumFromJSON(json: any): SamlVersionEnum {
return SamlVersionEnumFromJSONTyped(json, false);
}
export function SamlVersionEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): SamlVersionEnum {
return json as SamlVersionEnum;
}
export function SamlVersionEnumToJSON(value?: SamlVersionEnum | null): any {
return value as any;
}
export function SamlVersionEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): SamlVersionEnum {
return value as SamlVersionEnum;
}

View File

@@ -16,6 +16,8 @@ import type { DigestAlgorithmEnum } from "./DigestAlgorithmEnum";
import { DigestAlgorithmEnumFromJSON, DigestAlgorithmEnumToJSON } from "./DigestAlgorithmEnum";
import type { SAMLNameIDPolicyEnum } from "./SAMLNameIDPolicyEnum";
import { SAMLNameIDPolicyEnumFromJSON, SAMLNameIDPolicyEnumToJSON } from "./SAMLNameIDPolicyEnum";
import type { SamlVersionEnum } from "./SamlVersionEnum";
import { SamlVersionEnumFromJSON, SamlVersionEnumToJSON } from "./SamlVersionEnum";
import type { SignatureAlgorithmEnum } from "./SignatureAlgorithmEnum";
import {
SignatureAlgorithmEnumFromJSON,
@@ -154,6 +156,12 @@ export interface WSFederationProvider {
* @memberof WSFederationProvider
*/
authnContextClassRefMapping?: string | null;
/**
* SAML assertion version to issue in the security token. Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1.
* @type {SamlVersionEnum}
* @memberof WSFederationProvider
*/
samlVersion?: SamlVersionEnum;
/**
*
* @type {DigestAlgorithmEnum}
@@ -208,6 +216,12 @@ export interface WSFederationProvider {
* @memberof WSFederationProvider
*/
readonly urlWsfed: string;
/**
* Get Issuer/EntityID URL
* @type {string}
* @memberof WSFederationProvider
*/
readonly urlIssuer: string;
}
/**
@@ -241,6 +255,7 @@ export function instanceOfWSFederationProvider(value: object): value is WSFedera
if (!("urlDownloadMetadata" in value) || value["urlDownloadMetadata"] === undefined)
return false;
if (!("urlWsfed" in value) || value["urlWsfed"] === undefined) return false;
if (!("urlIssuer" in value) || value["urlIssuer"] === undefined) return false;
return true;
}
@@ -290,6 +305,10 @@ export function WSFederationProviderFromJSONTyped(
json["authn_context_class_ref_mapping"] == null
? undefined
: json["authn_context_class_ref_mapping"],
samlVersion:
json["saml_version"] == null
? undefined
: SamlVersionEnumFromJSON(json["saml_version"]),
digestAlgorithm:
json["digest_algorithm"] == null
? undefined
@@ -309,6 +328,7 @@ export function WSFederationProviderFromJSONTyped(
: SAMLNameIDPolicyEnumFromJSON(json["default_name_id_policy"]),
urlDownloadMetadata: json["url_download_metadata"],
urlWsfed: json["url_wsfed"],
urlIssuer: json["url_issuer"],
};
}
@@ -330,6 +350,7 @@ export function WSFederationProviderToJSONTyped(
| "meta_model_name"
| "url_download_metadata"
| "url_wsfed"
| "url_issuer"
> | null,
ignoreDiscriminator: boolean = false,
): any {
@@ -350,6 +371,7 @@ export function WSFederationProviderToJSONTyped(
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],
name_id_mapping: value["nameIdMapping"],
authn_context_class_ref_mapping: value["authnContextClassRefMapping"],
saml_version: SamlVersionEnumToJSON(value["samlVersion"]),
digest_algorithm: DigestAlgorithmEnumToJSON(value["digestAlgorithm"]),
signature_algorithm: SignatureAlgorithmEnumToJSON(value["signatureAlgorithm"]),
signing_kp: value["signingKp"],

View File

@@ -16,6 +16,8 @@ import type { DigestAlgorithmEnum } from "./DigestAlgorithmEnum";
import { DigestAlgorithmEnumFromJSON, DigestAlgorithmEnumToJSON } from "./DigestAlgorithmEnum";
import type { SAMLNameIDPolicyEnum } from "./SAMLNameIDPolicyEnum";
import { SAMLNameIDPolicyEnumFromJSON, SAMLNameIDPolicyEnumToJSON } from "./SAMLNameIDPolicyEnum";
import type { SamlVersionEnum } from "./SamlVersionEnum";
import { SamlVersionEnumFromJSON, SamlVersionEnumToJSON } from "./SamlVersionEnum";
import type { SignatureAlgorithmEnum } from "./SignatureAlgorithmEnum";
import {
SignatureAlgorithmEnumFromJSON,
@@ -100,6 +102,12 @@ export interface WSFederationProviderRequest {
* @memberof WSFederationProviderRequest
*/
authnContextClassRefMapping?: string | null;
/**
* SAML assertion version to issue in the security token. Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1.
* @type {SamlVersionEnum}
* @memberof WSFederationProviderRequest
*/
samlVersion?: SamlVersionEnum;
/**
*
* @type {DigestAlgorithmEnum}
@@ -195,6 +203,10 @@ export function WSFederationProviderRequestFromJSONTyped(
json["authn_context_class_ref_mapping"] == null
? undefined
: json["authn_context_class_ref_mapping"],
samlVersion:
json["saml_version"] == null
? undefined
: SamlVersionEnumFromJSON(json["saml_version"]),
digestAlgorithm:
json["digest_algorithm"] == null
? undefined
@@ -240,6 +252,7 @@ export function WSFederationProviderRequestToJSONTyped(
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],
name_id_mapping: value["nameIdMapping"],
authn_context_class_ref_mapping: value["authnContextClassRefMapping"],
saml_version: SamlVersionEnumToJSON(value["samlVersion"]),
digest_algorithm: DigestAlgorithmEnumToJSON(value["digestAlgorithm"]),
signature_algorithm: SignatureAlgorithmEnumToJSON(value["signatureAlgorithm"]),
signing_kp: value["signingKp"],

View File

@@ -761,6 +761,7 @@ export * from "./SSFProvider";
export * from "./SSFProviderRequest";
export * from "./SSFStream";
export * from "./SSFStreamStatusEnum";
export * from "./SamlVersionEnum";
export * from "./Schedule";
export * from "./ScheduleRequest";
export * from "./ScopeMapping";

View File

@@ -20412,6 +20412,14 @@ paths:
format: uuid
explode: true
style: form
- in: query
name: saml_version
schema:
allOf:
- $ref: '#/components/schemas/SamlVersionEnum'
description: |+
SAML assertion version to issue in the security token. Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1.
- $ref: '#/components/parameters/QuerySearch'
- in: query
name: session_valid_not_on_or_after
@@ -52034,6 +52042,12 @@ components:
description: Configure how the AuthnContextClassRef value will be created.
When left empty, the AuthnContextClassRef will be set based on which authentication
methods the user used to authenticate.
saml_version:
allOf:
- $ref: '#/components/schemas/SamlVersionEnum'
description: SAML assertion version to issue in the security token. Microsoft
Entra ID and classic ADFS-style relying parties typically require SAML
1.1.
digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm:
@@ -56025,6 +56039,11 @@ components:
- disabled
- disabled_deleted
type: string
SamlVersionEnum:
enum:
- '1.1'
- '2.0'
type: string
Schedule:
type: object
properties:
@@ -59165,6 +59184,12 @@ components:
description: Configure how the AuthnContextClassRef value will be created.
When left empty, the AuthnContextClassRef will be set based on which authentication
methods the user used to authenticate.
saml_version:
allOf:
- $ref: '#/components/schemas/SamlVersionEnum'
description: SAML assertion version to issue in the security token. Microsoft
Entra ID and classic ADFS-style relying parties typically require SAML
1.1.
digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm:
@@ -59198,6 +59223,10 @@ components:
type: string
description: Get WS-Fed url
readOnly: true
url_issuer:
type: string
description: Get Issuer/EntityID URL
readOnly: true
required:
- assigned_application_name
- assigned_application_slug
@@ -59211,6 +59240,7 @@ components:
- pk
- reply_url
- url_download_metadata
- url_issuer
- url_wsfed
- verbose_name
- verbose_name_plural
@@ -59278,6 +59308,12 @@ components:
description: Configure how the AuthnContextClassRef value will be created.
When left empty, the AuthnContextClassRef will be set based on which authentication
methods the user used to authenticate.
saml_version:
allOf:
- $ref: '#/components/schemas/SamlVersionEnum'
description: SAML assertion version to issue in the security token. Microsoft
Entra ID and classic ADFS-style relying parties typically require SAML
1.1.
digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm:

View File

@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- edited with XML Spy v3.5 NT (http://www.xmlspy.com) by Phill Hallam-Baker (VeriSign Inc.) -->
<schema targetNamespace="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified">
<import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="xmldsig-core-schema.xsd"/>
<annotation>
<documentation>
Document identifier: cs-sstc-schema-assertion-01
Location: http://www.oasis-open.org/committees/security/docs/
</documentation>
</annotation>
<simpleType name="IDType">
<restriction base="string"/>
</simpleType>
<simpleType name="IDReferenceType">
<restriction base="string"/>
</simpleType>
<simpleType name="DecisionType">
<restriction base="string">
<enumeration value="Permit"/>
<enumeration value="Deny"/>
<enumeration value="Indeterminate"/>
</restriction>
</simpleType>
<element name="AssertionIDReference" type="saml:IDReferenceType"/>
<element name="Assertion" type="saml:AssertionType"/>
<complexType name="AssertionType">
<sequence>
<element ref="saml:Conditions" minOccurs="0"/>
<element ref="saml:Advice" minOccurs="0"/>
<choice maxOccurs="unbounded">
<element ref="saml:Statement"/>
<element ref="saml:SubjectStatement"/>
<element ref="saml:AuthenticationStatement"/>
<element ref="saml:AuthorizationDecisionStatement"/>
<element ref="saml:AttributeStatement"/>
</choice>
<element ref="ds:Signature" minOccurs="0"/>
</sequence>
<attribute name="MajorVersion" type="integer" use="required"/>
<attribute name="MinorVersion" type="integer" use="required"/>
<attribute name="AssertionID" type="saml:IDType" use="required"/>
<attribute name="Issuer" type="string" use="required"/>
<attribute name="IssueInstant" type="dateTime" use="required"/>
</complexType>
<element name="Conditions" type="saml:ConditionsType"/>
<complexType name="ConditionsType">
<choice minOccurs="0" maxOccurs="unbounded">
<element ref="saml:AudienceRestrictionCondition"/>
<element ref="saml:Condition"/>
</choice>
<attribute name="NotBefore" type="dateTime" use="optional"/>
<attribute name="NotOnOrAfter" type="dateTime" use="optional"/>
</complexType>
<element name="Condition" type="saml:ConditionAbstractType"/>
<complexType name="ConditionAbstractType" abstract="true"/>
<element name="AudienceRestrictionCondition" type="saml:AudienceRestrictionConditionType"/>
<complexType name="AudienceRestrictionConditionType">
<complexContent>
<extension base="saml:ConditionAbstractType">
<sequence>
<element ref="saml:Audience" maxOccurs="unbounded"/>
</sequence>
</extension>
</complexContent>
</complexType>
<element name="Audience" type="anyURI"/>
<element name="Advice" type="saml:AdviceType"/>
<complexType name="AdviceType">
<choice minOccurs="0" maxOccurs="unbounded">
<element ref="saml:AssertionIDReference"/>
<element ref="saml:Assertion"/>
<any namespace="##other" processContents="lax"/>
</choice>
</complexType>
<element name="Statement" type="saml:StatementAbstractType"/>
<complexType name="StatementAbstractType" abstract="true"/>
<element name="SubjectStatement" type="saml:SubjectStatementAbstractType"/>
<complexType name="SubjectStatementAbstractType" abstract="true">
<complexContent>
<extension base="saml:StatementAbstractType">
<sequence>
<element ref="saml:Subject"/>
</sequence>
</extension>
</complexContent>
</complexType>
<element name="Subject" type="saml:SubjectType"/>
<complexType name="SubjectType">
<choice>
<sequence>
<element ref="saml:NameIdentifier"/>
<element ref="saml:SubjectConfirmation" minOccurs="0"/>
</sequence>
<element ref="saml:SubjectConfirmation"/>
</choice>
</complexType>
<element name="NameIdentifier" type="saml:NameIdentifierType"/>
<complexType name="NameIdentifierType">
<simpleContent>
<extension base="string">
<attribute name="NameQualifier" type="string" use="optional"/>
<attribute name="Format" type="anyURI" use="optional"/>
</extension>
</simpleContent>
</complexType>
<element name="SubjectConfirmation" type="saml:SubjectConfirmationType"/>
<complexType name="SubjectConfirmationType">
<sequence>
<element ref="saml:ConfirmationMethod" maxOccurs="unbounded"/>
<element ref="saml:SubjectConfirmationData" minOccurs="0"/>
<element ref="ds:KeyInfo" minOccurs="0"/>
</sequence>
</complexType>
<element name="SubjectConfirmationData" type="anyType"/>
<element name="ConfirmationMethod" type="anyURI"/>
<element name="AuthenticationStatement" type="saml:AuthenticationStatementType"/>
<complexType name="AuthenticationStatementType">
<complexContent>
<extension base="saml:SubjectStatementAbstractType">
<sequence>
<element ref="saml:SubjectLocality" minOccurs="0"/>
<element ref="saml:AuthorityBinding" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
<attribute name="AuthenticationMethod" type="anyURI" use="required"/>
<attribute name="AuthenticationInstant" type="dateTime" use="required"/>
</extension>
</complexContent>
</complexType>
<element name="SubjectLocality" type="saml:SubjectLocalityType"/>
<complexType name="SubjectLocalityType">
<attribute name="IPAddress" type="string" use="optional"/>
<attribute name="DNSAddress" type="string" use="optional"/>
</complexType>
<element name="AuthorityBinding" type="saml:AuthorityBindingType"/>
<complexType name="AuthorityBindingType">
<attribute name="AuthorityKind" type="QName" use="required"/>
<attribute name="Location" type="anyURI" use="required"/>
<attribute name="Binding" type="anyURI" use="required"/>
</complexType>
<element name="AuthorizationDecisionStatement" type="saml:AuthorizationDecisionStatementType"/>
<complexType name="AuthorizationDecisionStatementType">
<complexContent>
<extension base="saml:SubjectStatementAbstractType">
<sequence>
<element ref="saml:Action" maxOccurs="unbounded"/>
<element ref="saml:Evidence" minOccurs="0"/>
</sequence>
<attribute name="Resource" type="anyURI" use="required"/>
<attribute name="Decision" type="saml:DecisionType" use="required"/>
</extension>
</complexContent>
</complexType>
<element name="Action" type="saml:ActionType"/>
<complexType name="ActionType">
<simpleContent>
<extension base="string">
<attribute name="Namespace" type="anyURI"/>
</extension>
</simpleContent>
</complexType>
<element name="Evidence" type="saml:EvidenceType"/>
<complexType name="EvidenceType">
<choice maxOccurs="unbounded">
<element ref="saml:AssertionIDReference"/>
<element ref="saml:Assertion"/>
</choice>
</complexType>
<element name="AttributeStatement" type="saml:AttributeStatementType"/>
<complexType name="AttributeStatementType">
<complexContent>
<extension base="saml:SubjectStatementAbstractType">
<sequence>
<element ref="saml:Attribute" maxOccurs="unbounded"/>
</sequence>
</extension>
</complexContent>
</complexType>
<element name="AttributeDesignator" type="saml:AttributeDesignatorType"/>
<complexType name="AttributeDesignatorType">
<attribute name="AttributeName" type="string" use="required"/>
<attribute name="AttributeNamespace" type="anyURI" use="required"/>
</complexType>
<element name="Attribute" type="saml:AttributeType"/>
<complexType name="AttributeType">
<complexContent>
<extension base="saml:AttributeDesignatorType">
<sequence>
<element ref="saml:AttributeValue" maxOccurs="unbounded"/>
</sequence>
</extension>
</complexContent>
</complexType>
<element name="AttributeValue" type="anyType"/>
</schema>

View File

@@ -32,6 +32,7 @@ import {
PropertymappingsApi,
SAMLNameIDPolicyEnum,
SAMLPropertyMapping,
SamlVersionEnum,
ValidationError,
WSFederationProvider,
} from "@goauthentik/api";
@@ -40,6 +41,16 @@ import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
const samlVersionAndLabel = [
[
SamlVersionEnum._11,
msg("SAML 1.1 (required by Microsoft Entra ID / ADFS)", {
id: "wsfed.saml-version.option.saml11",
}),
],
[SamlVersionEnum._20, msg("SAML 2.0", { id: "wsfed.saml-version.option.saml20" })],
];
const samlNameIDPolicyAndLabel = [
[SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent, msg("Persistent")],
[SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress, msg("Email address")],
@@ -288,6 +299,33 @@ export function renderForm({
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("SAML assertion version", {
id: "wsfed.saml-version.label",
})}
required
name="samlVersion"
>
<select class="pf-c-form-control">
${samlVersionAndLabel.map(
([version, label]) => html`
<option
value=${version}
?selected=${provider?.samlVersion === version}
>
${label}
</option>
`,
)}
</select>
<p class="pf-c-form__helper-text">
${msg(
"Microsoft Entra ID and classic ADFS-style relying parties typically require SAML 1.1.",
{ id: "wsfed.saml-version.description" },
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Digest algorithm")}
required

View File

@@ -331,6 +331,19 @@ export class WSFederationProviderViewPage extends AKElement {
value="${ifDefined(this.provider.wtrealm)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("Issuer")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlIssuer)}"
/>
</div>
</form>
</div>
</div>`

View File

@@ -13,7 +13,8 @@ An authentik WS-Federation provider is typically created as part of an applicati
5. On the **Configure WS-Federation Provider** page, provide a name for the provider, select an authorization flow, and the two required configuration settings:
- **Reply URL**: Enter the application callback URL, where the token should be sent. This is the specific endpoint on an RP (application) where an Identity Provider (STS) sends the security token and authentication response after a successful login.
- **Realm**: Enter the identifier (string) of the requesting realm; that is, the Relying Party (RP) or application receiving the token. Realm is similar to the SAML 2.0 Entity ID.
6. Click **Create Application** to create both the application and the provider.
6. Under **Advanced protocol settings**, optionally set the **SAML assertion version**. This defaults to SAML 2.0; select SAML 1.1 if the relying party requires it, such as Microsoft Entra ID or a classic ADFS-style integration.
7. Click **Create Application** to create both the application and the provider.
## Export authentik WS-Federation provider metadata