mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
16 Commits
docs/invit
...
saml-sourc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf1392a89e | ||
|
|
69a86cf258 | ||
|
|
f79b1ba41e | ||
|
|
772db03b4b | ||
|
|
724f3cc59c | ||
|
|
99bf2ac131 | ||
|
|
dc9b302628 | ||
|
|
764e7a520c | ||
|
|
02e3baa84d | ||
|
|
46f17d23e9 | ||
|
|
3f832913dc | ||
|
|
f449335ad1 | ||
|
|
4215e76b74 | ||
|
|
ca63ee0142 | ||
|
|
63326b22bd | ||
|
|
8e3cff2769 |
@@ -1,4 +1,4 @@
|
||||
"""authentik SAML IDP Exceptions"""
|
||||
"""Common SAML Exceptions"""
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
0
authentik/common/saml/parsers/__init__.py
Normal file
0
authentik/common/saml/parsers/__init__.py
Normal file
@@ -1,4 +1,4 @@
|
||||
"""LogoutRequest parser"""
|
||||
"""Shared SAML LogoutRequest parser"""
|
||||
|
||||
from base64 import b64decode
|
||||
from dataclasses import dataclass
|
||||
@@ -6,41 +6,29 @@ from dataclasses import dataclass
|
||||
from defusedxml import ElementTree
|
||||
|
||||
from authentik.common.saml.constants import NS_SAML_ASSERTION, NS_SAML_PROTOCOL
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.processors.authn_request_parser import ERROR_CANNOT_DECODE_REQUEST
|
||||
from authentik.common.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LogoutRequest:
|
||||
"""Logout Request"""
|
||||
"""Parsed SAML LogoutRequest"""
|
||||
|
||||
id: str | None = None
|
||||
|
||||
issuer: str | None = None
|
||||
|
||||
name_id: str | None = None
|
||||
|
||||
name_id_format: str | None = None
|
||||
|
||||
session_index: str | None = None
|
||||
|
||||
relay_state: str | None = None
|
||||
|
||||
|
||||
class LogoutRequestParser:
|
||||
"""LogoutRequest Parser"""
|
||||
|
||||
provider: SAMLProvider
|
||||
|
||||
def __init__(self, provider: SAMLProvider):
|
||||
self.provider = provider
|
||||
"""Parse incoming SAML LogoutRequest messages"""
|
||||
|
||||
def _parse_xml(self, decoded_xml: str | bytes, relay_state: str | None = None) -> LogoutRequest:
|
||||
root = ElementTree.fromstring(decoded_xml)
|
||||
request = LogoutRequest(
|
||||
id=root.attrib["ID"],
|
||||
id=root.attrib.get("ID"),
|
||||
)
|
||||
# Try both namespaces for Issuer
|
||||
issuers = root.findall(f"{{{NS_SAML_PROTOCOL}}}Issuer")
|
||||
@@ -55,7 +43,6 @@ class LogoutRequestParser:
|
||||
name_ids = root.findall(f"{{{NS_SAML_PROTOCOL}}}NameID")
|
||||
if len(name_ids) > 0:
|
||||
request.name_id = name_ids[0].text
|
||||
# Extract NameID Format if present
|
||||
if "Format" in name_ids[0].attrib:
|
||||
request.name_id_format = name_ids[0].attrib["Format"]
|
||||
|
||||
@@ -70,22 +57,17 @@ class LogoutRequestParser:
|
||||
return request
|
||||
|
||||
def parse(self, saml_request: str, relay_state: str | None = None) -> LogoutRequest:
|
||||
"""Validate and parse raw request with enveloped signautre."""
|
||||
"""Parse a POST-binding LogoutRequest (base64 encoded)."""
|
||||
try:
|
||||
decoded_xml = b64decode(saml_request.encode())
|
||||
except UnicodeDecodeError:
|
||||
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST) from None
|
||||
raise CannotHandleAssertion("Cannot decode SAML request") from None
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
|
||||
def parse_detached(
|
||||
self,
|
||||
saml_request: str,
|
||||
relay_state: str | None = None,
|
||||
) -> LogoutRequest:
|
||||
"""Validate and parse raw request with detached signature"""
|
||||
def parse_detached(self, saml_request: str, relay_state: str | None = None) -> LogoutRequest:
|
||||
"""Parse a Redirect-binding LogoutRequest (deflate + base64 encoded)."""
|
||||
try:
|
||||
decoded_xml = decode_base64_and_inflate(saml_request)
|
||||
except UnicodeDecodeError:
|
||||
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST) from None
|
||||
|
||||
raise CannotHandleAssertion("Cannot decode SAML request") from None
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
43
authentik/common/saml/parsers/logout_response.py
Normal file
43
authentik/common/saml/parsers/logout_response.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Shared SAML LogoutResponse parser"""
|
||||
|
||||
from defusedxml.lxml import fromstring
|
||||
from lxml.etree import _Element # nosec
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.common.saml.constants import NS_SAML_PROTOCOL, SAML_STATUS_SUCCESS
|
||||
from authentik.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class LogoutResponseParser:
|
||||
"""Parse and validate SAML LogoutResponse messages"""
|
||||
|
||||
_root: _Element
|
||||
|
||||
def __init__(self, raw_response: str):
|
||||
self._raw_response = raw_response
|
||||
|
||||
def parse(self):
|
||||
"""Decode and parse the LogoutResponse XML."""
|
||||
# decode_base64_and_inflate handles both deflate-compressed (Redirect binding)
|
||||
# and plain base64 (POST binding) responses
|
||||
response_xml = decode_base64_and_inflate(self._raw_response)
|
||||
self._root = fromstring(response_xml.encode())
|
||||
|
||||
def verify_status(self) -> bool:
|
||||
"""Check LogoutResponse status. Returns True if status is Success."""
|
||||
status = self._root.find(f"{{{NS_SAML_PROTOCOL}}}Status")
|
||||
if status is None:
|
||||
return True
|
||||
status_code = status.find(f"{{{NS_SAML_PROTOCOL}}}StatusCode")
|
||||
if status_code is None:
|
||||
return True
|
||||
status_value = status_code.attrib.get("Value", "")
|
||||
if status_value != SAML_STATUS_SUCCESS:
|
||||
LOGGER.warning(
|
||||
"LogoutResponse status is not Success",
|
||||
status=status_value,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
@@ -19,8 +19,8 @@ from authentik.common.saml.constants import (
|
||||
RSA_SHA512,
|
||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||
)
|
||||
from authentik.common.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
from authentik.sources.saml.models import SAMLNameIDPolicy
|
||||
|
||||
@@ -15,8 +15,8 @@ from authentik.common.saml.constants import (
|
||||
NS_SAML_PROTOCOL,
|
||||
SIGN_ALGORITHM_TRANSFORM_MAP,
|
||||
)
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequest
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
|
||||
from authentik.providers.saml.utils import get_random_id
|
||||
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
|
||||
from authentik.providers.saml.utils.time import get_time_string
|
||||
|
||||
@@ -5,10 +5,10 @@ from django.contrib.auth import get_user_model
|
||||
from dramatiq.actor import actor
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequest
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
|
||||
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -8,10 +8,10 @@ from authentik.common.saml.constants import (
|
||||
RSA_SHA256,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
)
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequestParser
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
|
||||
|
||||
|
||||
class TestLogoutIntegration(TestCase):
|
||||
@@ -46,7 +46,7 @@ class TestLogoutIntegration(TestCase):
|
||||
)
|
||||
|
||||
# Create parser for validation
|
||||
self.parser = LogoutRequestParser(self.provider)
|
||||
self.parser = LogoutRequestParser()
|
||||
|
||||
def test_post_binding_roundtrip(self):
|
||||
"""Test that a POST-encoded request can be parsed correctly"""
|
||||
@@ -100,7 +100,7 @@ class TestLogoutIntegration(TestCase):
|
||||
encoded = processor.encode_post()
|
||||
|
||||
# Create parser with verification enabled
|
||||
parser = LogoutRequestParser(self.provider)
|
||||
parser = LogoutRequestParser()
|
||||
|
||||
# Parse it - this would validate signature if verification is enabled
|
||||
parsed = parser.parse(encoded)
|
||||
|
||||
@@ -4,9 +4,9 @@ from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_TRANSIENT
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequestParser
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
||||
GET_LOGOUT_REQUEST = (
|
||||
@@ -51,7 +51,7 @@ class TestLogoutRequest(TestCase):
|
||||
|
||||
def test_static_get(self):
|
||||
"""Test static LogoutRequest"""
|
||||
request = LogoutRequestParser(self.provider).parse_detached(GET_LOGOUT_REQUEST)
|
||||
request = LogoutRequestParser().parse_detached(GET_LOGOUT_REQUEST)
|
||||
self.assertEqual(request.id, "id-2ea1b01f69363ac95e3da4a15409b9d8ec525944")
|
||||
self.assertEqual(request.issuer, "saml-test-sp")
|
||||
# The GET request has an empty NameID element with transient format
|
||||
@@ -60,7 +60,7 @@ class TestLogoutRequest(TestCase):
|
||||
|
||||
def test_static_post(self):
|
||||
"""Test static LogoutRequest"""
|
||||
request = LogoutRequestParser(self.provider).parse(POST_LOGOUT_REQUEST)
|
||||
request = LogoutRequestParser().parse(POST_LOGOUT_REQUEST)
|
||||
self.assertEqual(request.id, "id-b8f4fd51ed4106f1e782b95d51d9ad3f385e5816")
|
||||
self.assertEqual(request.issuer, "saml-test-sp")
|
||||
# The POST request has an empty NameID element with transient format
|
||||
|
||||
@@ -9,11 +9,11 @@ from authentik.common.saml.constants import (
|
||||
NS_SAML_PROTOCOL,
|
||||
NS_SIGNATURE,
|
||||
)
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequest
|
||||
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
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_EMAIL
|
||||
from authentik.common.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_brand, create_test_cert, create_test_flow
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLLogoutMethods, SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
|
||||
from authentik.providers.saml.views.flows import (
|
||||
|
||||
@@ -7,6 +7,8 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.common.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequestParser
|
||||
from authentik.core.models import Application, AuthenticatedSession
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow, in_memory_stage
|
||||
@@ -16,7 +18,6 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.iframe_logout import IframeLogoutStageView
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import (
|
||||
SAMLBindings,
|
||||
SAMLLogoutMethods,
|
||||
@@ -24,7 +25,6 @@ from authentik.providers.saml.models import (
|
||||
SAMLSession,
|
||||
)
|
||||
from authentik.providers.saml.native_logout import NativeLogoutStageView
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
|
||||
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
|
||||
from authentik.providers.saml.tasks import send_saml_logout_response
|
||||
from authentik.providers.saml.utils.encoding import nice64
|
||||
@@ -251,7 +251,7 @@ class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
|
||||
return bad_request_message(self.request, "The SAML request payload is missing.")
|
||||
|
||||
try:
|
||||
logout_request = LogoutRequestParser(self.provider).parse_detached(
|
||||
logout_request = LogoutRequestParser().parse_detached(
|
||||
self.request.GET[REQUEST_KEY_SAML_REQUEST],
|
||||
relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
|
||||
)
|
||||
@@ -295,7 +295,7 @@ class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
|
||||
return bad_request_message(self.request, "The SAML request payload is missing.")
|
||||
|
||||
try:
|
||||
logout_request = LogoutRequestParser(self.provider).parse(
|
||||
logout_request = LogoutRequestParser().parse(
|
||||
payload[REQUEST_KEY_SAML_REQUEST],
|
||||
relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.common.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
@@ -16,7 +17,6 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
|
||||
from authentik.flows.views.executor import SESSION_KEY_POST
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
||||
from authentik.providers.saml.views.flows import (
|
||||
|
||||
@@ -43,6 +43,7 @@ class SAMLSourceSerializer(SourceSerializer):
|
||||
"force_authn",
|
||||
"name_id_policy",
|
||||
"binding_type",
|
||||
"slo_binding",
|
||||
"verification_kp",
|
||||
"signing_kp",
|
||||
"digest_algorithm",
|
||||
@@ -51,6 +52,8 @@ class SAMLSourceSerializer(SourceSerializer):
|
||||
"encryption_kp",
|
||||
"signed_assertion",
|
||||
"signed_response",
|
||||
"sign_authn_request",
|
||||
"sign_logout_request",
|
||||
]
|
||||
|
||||
|
||||
@@ -78,6 +81,7 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"force_authn",
|
||||
"name_id_policy",
|
||||
"binding_type",
|
||||
"slo_binding",
|
||||
"verification_kp",
|
||||
"signing_kp",
|
||||
"digest_algorithm",
|
||||
@@ -85,6 +89,8 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"temporary_user_delete_after",
|
||||
"signed_assertion",
|
||||
"signed_response",
|
||||
"sign_authn_request",
|
||||
"sign_logout_request",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-09 22:34
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
|
||||
("authentik_sources_saml", "0021_samlsource_signed_assertion_and_more"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="samlsource",
|
||||
name="sign_authn_request",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set.",
|
||||
verbose_name="Sign AuthnRequest",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="samlsource",
|
||||
name="sign_logout_request",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set.",
|
||||
verbose_name="Sign LogoutRequest",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="samlsource",
|
||||
name="slo_binding",
|
||||
field=models.CharField(
|
||||
choices=[("REDIRECT", "Redirect Binding"), ("POST", "POST Binding")],
|
||||
default="REDIRECT",
|
||||
help_text="Binding type for Single Logout requests to the IdP.",
|
||||
max_length=100,
|
||||
verbose_name="SLO Binding",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SAMLSourceSession",
|
||||
fields=[
|
||||
(
|
||||
"saml_session_id",
|
||||
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
|
||||
),
|
||||
(
|
||||
"session_index",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="SAML SessionIndex from the IdP's AuthnStatement",
|
||||
),
|
||||
),
|
||||
("name_id", models.TextField(help_text="SAML NameID value for this session")),
|
||||
(
|
||||
"name_id_format",
|
||||
models.TextField(blank=True, default="", help_text="SAML NameID format"),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"session",
|
||||
models.ForeignKey(
|
||||
help_text="Link to the user's authenticated session",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_core.authenticatedsession",
|
||||
),
|
||||
),
|
||||
(
|
||||
"source",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_sources_saml.samlsource",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="User",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "SAML Source Session",
|
||||
"verbose_name_plural": "SAML Source Sessions",
|
||||
"indexes": [
|
||||
models.Index(fields=["source", "user"], name="authentik_s_source__abd088_idx"),
|
||||
models.Index(fields=["session"], name="authentik_s_session_054d2d_idx"),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
"""saml sp models"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
@@ -36,9 +37,11 @@ from authentik.common.saml.constants import (
|
||||
SHA512,
|
||||
)
|
||||
from authentik.core.models import (
|
||||
AuthenticatedSession,
|
||||
GroupSourceConnection,
|
||||
PropertyMapping,
|
||||
Source,
|
||||
User,
|
||||
UserSourceConnection,
|
||||
)
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
@@ -78,6 +81,13 @@ class SAMLNameIDPolicy(models.TextChoices):
|
||||
UNSPECIFIED = SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||
|
||||
|
||||
class SAMLSLOBindingTypes(models.TextChoices):
|
||||
"""SAML SLO Binding types"""
|
||||
|
||||
REDIRECT = "REDIRECT", _("Redirect Binding")
|
||||
POST = "POST", _("POST Binding")
|
||||
|
||||
|
||||
class SAMLSource(Source):
|
||||
"""Authenticate using an external SAML Identity Provider."""
|
||||
|
||||
@@ -134,6 +144,28 @@ class SAMLSource(Source):
|
||||
choices=SAMLBindingTypes.choices,
|
||||
default=SAMLBindingTypes.REDIRECT,
|
||||
)
|
||||
slo_binding = models.CharField(
|
||||
max_length=100,
|
||||
choices=SAMLSLOBindingTypes.choices,
|
||||
default=SAMLSLOBindingTypes.REDIRECT,
|
||||
verbose_name=_("SLO Binding"),
|
||||
help_text=_("Binding type for Single Logout requests to the IdP."),
|
||||
)
|
||||
|
||||
sign_authn_request = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Sign AuthnRequest"),
|
||||
help_text=_(
|
||||
"Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set."
|
||||
),
|
||||
)
|
||||
sign_logout_request = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Sign LogoutRequest"),
|
||||
help_text=_(
|
||||
"Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set."
|
||||
),
|
||||
)
|
||||
|
||||
temporary_user_delete_after = models.TextField(
|
||||
default="days=1",
|
||||
@@ -355,3 +387,39 @@ class GroupSAMLSourceConnection(GroupSourceConnection):
|
||||
class Meta:
|
||||
verbose_name = _("Group SAML Source Connection")
|
||||
verbose_name_plural = _("Group SAML Source Connections")
|
||||
|
||||
|
||||
class SAMLSourceSession(models.Model):
|
||||
"""Track active SAML source sessions for Single Logout support"""
|
||||
|
||||
saml_session_id = models.UUIDField(default=uuid4, primary_key=True)
|
||||
source = models.ForeignKey(SAMLSource, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||
session = models.ForeignKey(
|
||||
AuthenticatedSession,
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_("Link to the user's authenticated session"),
|
||||
)
|
||||
session_index = models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
help_text=_("SAML SessionIndex from the IdP's AuthnStatement"),
|
||||
)
|
||||
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"),
|
||||
)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("SAML Source Session")
|
||||
verbose_name_plural = _("SAML Source Sessions")
|
||||
indexes = [
|
||||
models.Index(fields=["source", "user"]),
|
||||
models.Index(fields=["session"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"SAML Source Session for source {self.source_id} and user {self.user_id}"
|
||||
|
||||
210
authentik/sources/saml/processors/logout_request.py
Normal file
210
authentik/sources/saml/processors/logout_request.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""SAML Source LogoutRequest Processor"""
|
||||
|
||||
import base64
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
import xmlsec
|
||||
from django.http import HttpRequest
|
||||
from lxml import etree # nosec
|
||||
from lxml.etree import Element, _Element
|
||||
|
||||
from authentik.common.saml.constants import (
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
NS_SAML_PROTOCOL,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SIGN_ALGORITHM_TRANSFORM_MAP,
|
||||
)
|
||||
from authentik.lib.xml import remove_xml_newlines
|
||||
from authentik.providers.saml.utils import get_random_id
|
||||
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
|
||||
from authentik.providers.saml.utils.time import get_time_string
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
||||
|
||||
class LogoutRequestProcessor:
|
||||
"""Generate SAML LogoutRequest messages for SP-initiated logout"""
|
||||
|
||||
source: SAMLSource
|
||||
http_request: HttpRequest
|
||||
destination: str
|
||||
name_id: str
|
||||
name_id_format: str
|
||||
session_index: str
|
||||
relay_state: str | None
|
||||
|
||||
_issue_instant: str
|
||||
_request_id: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source: SAMLSource,
|
||||
http_request: HttpRequest,
|
||||
destination: str,
|
||||
name_id: str,
|
||||
name_id_format: str = SAML_NAME_ID_FORMAT_EMAIL,
|
||||
session_index: str = "",
|
||||
relay_state: str | None = None,
|
||||
):
|
||||
self.source = source
|
||||
self.http_request = http_request
|
||||
self.destination = destination
|
||||
self.name_id = name_id
|
||||
self.name_id_format = name_id_format
|
||||
self.session_index = session_index
|
||||
self.relay_state = relay_state
|
||||
|
||||
self._issue_instant = get_time_string()
|
||||
self._request_id = get_random_id()
|
||||
|
||||
def get_issuer(self) -> Element:
|
||||
"""Get Issuer element"""
|
||||
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
|
||||
issuer.text = self.source.get_issuer(self.http_request)
|
||||
return issuer
|
||||
|
||||
def get_name_id(self) -> Element:
|
||||
"""Get NameID element"""
|
||||
name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID")
|
||||
name_id.attrib["Format"] = self.name_id_format
|
||||
name_id.text = self.name_id
|
||||
return name_id
|
||||
|
||||
def build(self) -> Element:
|
||||
"""Build a SAML LogoutRequest as etree Element"""
|
||||
logout_request = Element(f"{{{NS_SAML_PROTOCOL}}}LogoutRequest", nsmap=NS_MAP)
|
||||
logout_request.attrib["ID"] = self._request_id
|
||||
logout_request.attrib["Version"] = "2.0"
|
||||
logout_request.attrib["IssueInstant"] = self._issue_instant
|
||||
logout_request.attrib["Destination"] = self.destination
|
||||
|
||||
logout_request.append(self.get_issuer())
|
||||
logout_request.append(self.get_name_id())
|
||||
|
||||
if self.session_index:
|
||||
session_index_element = Element(f"{{{NS_SAML_PROTOCOL}}}SessionIndex")
|
||||
session_index_element.text = self.session_index
|
||||
logout_request.append(session_index_element)
|
||||
|
||||
return logout_request
|
||||
|
||||
def encode_post(self) -> str:
|
||||
"""Encode LogoutRequest for POST binding"""
|
||||
logout_request = self.build()
|
||||
if self.source.signing_kp and self.source.sign_logout_request:
|
||||
self._sign_logout_request(logout_request)
|
||||
return base64.b64encode(etree.tostring(logout_request)).decode()
|
||||
|
||||
def encode_redirect(self) -> str:
|
||||
"""Encode LogoutRequest for Redirect binding"""
|
||||
logout_request = self.build()
|
||||
xml_str = etree.tostring(logout_request, encoding="UTF-8", xml_declaration=True)
|
||||
return deflate_and_base64_encode(xml_str.decode("UTF-8"))
|
||||
|
||||
def get_redirect_url(self) -> str:
|
||||
"""Build complete logout URL for redirect binding with signature if needed"""
|
||||
encoded_request = self.encode_redirect()
|
||||
params = {
|
||||
"SAMLRequest": encoded_request,
|
||||
}
|
||||
|
||||
if self.relay_state:
|
||||
params["RelayState"] = self.relay_state
|
||||
|
||||
if self.source.signing_kp and self.source.sign_logout_request:
|
||||
sig_alg = self.source.signature_algorithm
|
||||
params["SigAlg"] = sig_alg
|
||||
|
||||
query_string = self._build_signable_query_string(params)
|
||||
signature = self._sign_query_string(query_string)
|
||||
params["Signature"] = base64.b64encode(signature).decode()
|
||||
|
||||
separator = "&" if "?" in self.destination else "?"
|
||||
return f"{self.destination}{separator}{urlencode(params)}"
|
||||
|
||||
def get_post_form_data(self) -> dict:
|
||||
"""Get form data for POST binding"""
|
||||
return {
|
||||
"SAMLRequest": self.encode_post(),
|
||||
"RelayState": self.relay_state or "",
|
||||
}
|
||||
|
||||
def _sign_logout_request(self, logout_request: _Element):
|
||||
"""Sign the LogoutRequest element"""
|
||||
signature_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
signature = xmlsec.template.create(
|
||||
logout_request,
|
||||
xmlsec.constants.TransformExclC14N,
|
||||
signature_algorithm_transform,
|
||||
ns=xmlsec.constants.DSigNs,
|
||||
)
|
||||
|
||||
issuer = logout_request.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
|
||||
if issuer is not None:
|
||||
issuer.addnext(signature)
|
||||
else:
|
||||
logout_request.insert(0, signature)
|
||||
|
||||
self._sign(logout_request)
|
||||
|
||||
def _sign(self, element: _Element):
|
||||
"""Sign an XML element based on the source's configured signing settings"""
|
||||
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
|
||||
self.source.digest_algorithm, xmlsec.constants.TransformSha1
|
||||
)
|
||||
xmlsec.tree.add_ids(element, ["ID"])
|
||||
signature_node = xmlsec.tree.find_node(element, xmlsec.constants.NodeSignature)
|
||||
ref = xmlsec.template.add_reference(
|
||||
signature_node,
|
||||
digest_algorithm_transform,
|
||||
uri="#" + element.attrib["ID"],
|
||||
)
|
||||
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.source.signing_kp.key_data,
|
||||
xmlsec.constants.KeyDataFormatPem,
|
||||
None,
|
||||
)
|
||||
key.load_cert_from_memory(
|
||||
self.source.signing_kp.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatCertPem,
|
||||
)
|
||||
ctx.key = key
|
||||
ctx.sign(remove_xml_newlines(element, signature_node))
|
||||
|
||||
def _build_signable_query_string(self, params: dict) -> str:
|
||||
"""Build query string for signing (order matters per SAML spec)"""
|
||||
ordered = []
|
||||
if "SAMLRequest" in params:
|
||||
ordered.append(f"SAMLRequest={quote(params['SAMLRequest'], safe='')}")
|
||||
if "RelayState" in params:
|
||||
ordered.append(f"RelayState={quote(params['RelayState'], safe='')}")
|
||||
if "SigAlg" in params:
|
||||
ordered.append(f"SigAlg={quote(params['SigAlg'], safe='')}")
|
||||
return "&".join(ordered)
|
||||
|
||||
def _sign_query_string(self, query_string: str) -> bytes:
|
||||
"""Sign the query string for redirect binding"""
|
||||
signature_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha256
|
||||
)
|
||||
|
||||
key = xmlsec.Key.from_memory(
|
||||
self.source.signing_kp.key_data,
|
||||
xmlsec.constants.KeyDataFormatPem,
|
||||
None,
|
||||
)
|
||||
|
||||
ctx = xmlsec.SignatureContext()
|
||||
ctx.key = key
|
||||
|
||||
return ctx.sign_binary(query_string.encode("utf-8"), signature_algorithm_transform)
|
||||
96
authentik/sources/saml/processors/logout_response.py
Normal file
96
authentik/sources/saml/processors/logout_response.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""SAML Source LogoutResponse Builder"""
|
||||
|
||||
import base64
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.http import HttpRequest
|
||||
from lxml import etree # nosec
|
||||
from lxml.etree import Element
|
||||
|
||||
from authentik.common.saml.constants import (
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
NS_SAML_PROTOCOL,
|
||||
SAML_STATUS_SUCCESS,
|
||||
)
|
||||
from authentik.providers.saml.utils import get_random_id
|
||||
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
|
||||
from authentik.providers.saml.utils.time import get_time_string
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
||||
|
||||
class LogoutResponseBuilder:
|
||||
"""Build SAML LogoutResponse messages for IdP-initiated logout"""
|
||||
|
||||
source: SAMLSource
|
||||
http_request: HttpRequest
|
||||
destination: str
|
||||
in_response_to: str
|
||||
|
||||
_issue_instant: str
|
||||
_response_id: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source: SAMLSource,
|
||||
http_request: HttpRequest,
|
||||
destination: str,
|
||||
in_response_to: str,
|
||||
):
|
||||
self.source = source
|
||||
self.http_request = http_request
|
||||
self.destination = destination
|
||||
self.in_response_to = in_response_to
|
||||
|
||||
self._issue_instant = get_time_string()
|
||||
self._response_id = get_random_id()
|
||||
|
||||
def build(self) -> Element:
|
||||
"""Build a SAML LogoutResponse as etree Element"""
|
||||
response = Element(f"{{{NS_SAML_PROTOCOL}}}LogoutResponse", nsmap=NS_MAP)
|
||||
response.attrib["ID"] = self._response_id
|
||||
response.attrib["Version"] = "2.0"
|
||||
response.attrib["IssueInstant"] = self._issue_instant
|
||||
response.attrib["Destination"] = self.destination
|
||||
response.attrib["InResponseTo"] = self.in_response_to
|
||||
|
||||
# Issuer
|
||||
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
|
||||
issuer.text = self.source.get_issuer(self.http_request)
|
||||
response.append(issuer)
|
||||
|
||||
# Status
|
||||
status = Element(f"{{{NS_SAML_PROTOCOL}}}Status")
|
||||
status_code = Element(f"{{{NS_SAML_PROTOCOL}}}StatusCode")
|
||||
status_code.attrib["Value"] = SAML_STATUS_SUCCESS
|
||||
status.append(status_code)
|
||||
response.append(status)
|
||||
|
||||
return response
|
||||
|
||||
def encode_post(self) -> str:
|
||||
"""Encode LogoutResponse for POST binding"""
|
||||
response = self.build()
|
||||
return base64.b64encode(etree.tostring(response)).decode()
|
||||
|
||||
def encode_redirect(self) -> str:
|
||||
"""Encode LogoutResponse for Redirect binding"""
|
||||
response = self.build()
|
||||
xml_str = etree.tostring(response, encoding="UTF-8", xml_declaration=True)
|
||||
return deflate_and_base64_encode(xml_str.decode("UTF-8"))
|
||||
|
||||
def get_redirect_url(self, relay_state: str | None = None) -> str:
|
||||
"""Build complete URL for redirect binding"""
|
||||
encoded = self.encode_redirect()
|
||||
params = {"SAMLResponse": encoded}
|
||||
if relay_state:
|
||||
params["RelayState"] = relay_state
|
||||
separator = "&" if "?" in self.destination else "?"
|
||||
return f"{self.destination}{separator}{urlencode(params)}"
|
||||
|
||||
def get_post_form_data(self, relay_state: str | None = None) -> dict:
|
||||
"""Get form data for POST binding"""
|
||||
data = {"SAMLResponse": self.encode_post()}
|
||||
if relay_state:
|
||||
data["RelayState"] = relay_state
|
||||
return data
|
||||
@@ -8,6 +8,7 @@ from authentik.common.saml.constants import (
|
||||
NS_SAML_METADATA,
|
||||
NS_SIGNATURE,
|
||||
SAML_BINDING_POST,
|
||||
SAML_BINDING_REDIRECT,
|
||||
)
|
||||
from authentik.providers.saml.utils.encoding import strip_pem_header
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
@@ -75,6 +76,19 @@ class MetadataProcessor:
|
||||
if encryption_descriptor is not None:
|
||||
sp_sso_descriptor.append(encryption_descriptor)
|
||||
|
||||
if self.source.slo_url:
|
||||
slo_location = self.source.build_full_url(self.http_request, view="slo")
|
||||
|
||||
slo_redirect = SubElement(
|
||||
sp_sso_descriptor, f"{{{NS_SAML_METADATA}}}SingleLogoutService"
|
||||
)
|
||||
slo_redirect.attrib["Binding"] = SAML_BINDING_REDIRECT
|
||||
slo_redirect.attrib["Location"] = slo_location
|
||||
|
||||
slo_post = SubElement(sp_sso_descriptor, f"{{{NS_SAML_METADATA}}}SingleLogoutService")
|
||||
slo_post.attrib["Binding"] = SAML_BINDING_POST
|
||||
slo_post.attrib["Location"] = slo_location
|
||||
|
||||
sp_sso_descriptor.append(self.get_name_id_format())
|
||||
|
||||
assertion_consumer_service = SubElement(
|
||||
|
||||
@@ -72,7 +72,11 @@ class RequestProcessor:
|
||||
# Create issuer object
|
||||
auth_n_request.append(self.get_issuer())
|
||||
|
||||
if self.source.signing_kp and self.source.binding_type != SAMLBindingTypes.REDIRECT:
|
||||
if (
|
||||
self.source.signing_kp
|
||||
and self.source.sign_authn_request
|
||||
and self.source.binding_type != SAMLBindingTypes.REDIRECT
|
||||
):
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
@@ -93,7 +97,11 @@ class RequestProcessor:
|
||||
(used for POST Bindings)"""
|
||||
auth_n_request = self.get_auth_n()
|
||||
|
||||
if self.source.signing_kp and self.source.binding_type != SAMLBindingTypes.REDIRECT:
|
||||
if (
|
||||
self.source.signing_kp
|
||||
and self.source.sign_authn_request
|
||||
and self.source.binding_type != SAMLBindingTypes.REDIRECT
|
||||
):
|
||||
xmlsec.tree.add_ids(auth_n_request, ["ID"])
|
||||
|
||||
ctx = xmlsec.SignatureContext()
|
||||
@@ -141,7 +149,7 @@ class RequestProcessor:
|
||||
if self.relay_state != "":
|
||||
response_dict["RelayState"] = self.relay_state
|
||||
|
||||
if self.source.signing_kp:
|
||||
if self.source.signing_kp and self.source.sign_authn_request:
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
|
||||
@@ -42,12 +42,9 @@ from authentik.sources.saml.exceptions import (
|
||||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from authentik.sources.saml.models import (
|
||||
GroupSAMLSourceConnection,
|
||||
SAMLSource,
|
||||
UserSAMLSourceConnection,
|
||||
)
|
||||
from authentik.sources.saml.models import SAMLSource, UserSAMLSourceConnection
|
||||
from authentik.sources.saml.processors.request import SESSION_KEY_REQUEST_ID
|
||||
from authentik.sources.saml.stages import PLAN_CONTEXT_SAML_SESSION_DATA, SAMLSourceFlowManager
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
@@ -240,6 +237,7 @@ class ResponseProcessor:
|
||||
UserSAMLSourceConnection.objects.create(
|
||||
source=self._source, user=user, identifier=name_id.text
|
||||
)
|
||||
session_index = self._get_session_index()
|
||||
return SAMLSourceFlowManager(
|
||||
source=self._source,
|
||||
request=self._http_request,
|
||||
@@ -249,9 +247,25 @@ class ResponseProcessor:
|
||||
"assertion": self.get_assertion(),
|
||||
"name_id": name_id,
|
||||
},
|
||||
policy_context={},
|
||||
policy_context={
|
||||
PLAN_CONTEXT_SAML_SESSION_DATA: {
|
||||
"session_index": session_index or "",
|
||||
"name_id": name_id.text,
|
||||
"name_id_format": name_id.attrib.get("Format", ""),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _get_session_index(self) -> str | None:
|
||||
"""Get SessionIndex from AuthnStatement element"""
|
||||
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
|
||||
if assertion is None:
|
||||
return None
|
||||
authn_statement = assertion.find(f"{{{NS_SAML_ASSERTION}}}AuthnStatement")
|
||||
if authn_statement is None:
|
||||
return None
|
||||
return authn_statement.attrib.get("SessionIndex")
|
||||
|
||||
def get_assertion(self) -> Element | None:
|
||||
"""Get assertion element, if we have a signed assertion"""
|
||||
if self._assertion is not None:
|
||||
@@ -307,6 +321,7 @@ class ResponseProcessor:
|
||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
||||
return self._handle_name_id_transient()
|
||||
|
||||
session_index = self._get_session_index()
|
||||
return SAMLSourceFlowManager(
|
||||
source=self._source,
|
||||
request=self._http_request,
|
||||
@@ -318,12 +333,10 @@ class ResponseProcessor:
|
||||
},
|
||||
policy_context={
|
||||
"saml_response": etree.tostring(self._root),
|
||||
PLAN_CONTEXT_SAML_SESSION_DATA: {
|
||||
"session_index": session_index or "",
|
||||
"name_id": name_id.text,
|
||||
"name_id_format": name_id.attrib.get("Format", ""),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SAMLSourceFlowManager(SourceFlowManager):
|
||||
"""Source flow manager for SAML Sources"""
|
||||
|
||||
user_connection_type = UserSAMLSourceConnection
|
||||
group_connection_type = GroupSAMLSourceConnection
|
||||
|
||||
@@ -3,12 +3,40 @@
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_DELETE_ON_LOGOUT, User
|
||||
from authentik.core.models import USER_ATTRIBUTE_DELETE_ON_LOGOUT, AuthenticatedSession, User
|
||||
from authentik.flows.challenge import PLAN_CONTEXT_ATTRS, PLAN_CONTEXT_TITLE, PLAN_CONTEXT_URL
|
||||
from authentik.flows.models import in_memory_stage
|
||||
from authentik.flows.stage import RedirectStage, SessionEndStage
|
||||
from authentik.flows.views.executor import FlowExecutorView
|
||||
from authentik.providers.saml.native_logout import NativeLogoutStageView
|
||||
from authentik.sources.saml.models import SAMLSLOBindingTypes, SAMLSourceSession
|
||||
from authentik.sources.saml.processors.logout_request import LogoutRequestProcessor
|
||||
from authentik.sources.saml.views import PLAN_CONTEXT_SAML_RELAY_STATE, AutosubmitStageView
|
||||
from authentik.stages.user_logout.models import UserLogoutStage
|
||||
from authentik.stages.user_logout.stage import flow_pre_user_logout
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
# Stages that redirect the user away from authentik. Source SLO stages must be
|
||||
# inserted before these so they have a chance to execute.
|
||||
TERMINAL_STAGE_VIEWS = {SessionEndStage, NativeLogoutStageView}
|
||||
|
||||
|
||||
def _insert_before_terminal_stage(plan, stage):
|
||||
"""Insert a stage before any terminal stage (SessionEndStage, NativeLogoutStageView)
|
||||
in the plan. Falls back to append if no terminal stage is found."""
|
||||
for i, binding in enumerate(plan.bindings):
|
||||
try:
|
||||
if binding.stage.view in TERMINAL_STAGE_VIEWS:
|
||||
plan.insert_stage(stage, index=i)
|
||||
return
|
||||
except NotImplementedError:
|
||||
continue
|
||||
plan.append_stage(stage)
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
||||
@@ -18,3 +46,89 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
||||
if user.attributes.get(USER_ATTRIBUTE_DELETE_ON_LOGOUT, False):
|
||||
LOGGER.debug("Deleted temporary user", user=user)
|
||||
user.delete()
|
||||
|
||||
|
||||
@receiver(flow_pre_user_logout)
|
||||
def handle_saml_source_pre_user_logout(
|
||||
sender, request: HttpRequest, user: User, executor: FlowExecutorView, **kwargs
|
||||
):
|
||||
"""Handle SAML source SP-initiated SLO when user logs out via flow.
|
||||
Injects a stage into the logout flow to redirect the user to the IdP's SLO URL."""
|
||||
if not isinstance(executor.current_stage, UserLogoutStage):
|
||||
return
|
||||
|
||||
if not user.is_authenticated:
|
||||
return
|
||||
|
||||
auth_session = AuthenticatedSession.from_request(request, user)
|
||||
if not auth_session:
|
||||
return
|
||||
|
||||
# Find SAMLSourceSessions for this user's current session
|
||||
saml_source_sessions = SAMLSourceSession.objects.filter(
|
||||
session=auth_session,
|
||||
user=user,
|
||||
).select_related("source")
|
||||
|
||||
for saml_session in saml_source_sessions:
|
||||
source = saml_session.source
|
||||
if not source.slo_url or not source.enabled:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Use the flow executor URL as relay_state so that after the IdP
|
||||
# processes the LogoutRequest and sends a LogoutResponse, the user
|
||||
# is redirected back to the flow to continue remaining stages.
|
||||
relay_state = request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_core:if-flow",
|
||||
kwargs={"flow_slug": executor.flow.slug},
|
||||
)
|
||||
)
|
||||
|
||||
# Stash the outbound relay_state so the SLOView can redirect to a
|
||||
# server-known value rather than trusting the echoed request param.
|
||||
executor.plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
|
||||
|
||||
processor = LogoutRequestProcessor(
|
||||
source=source,
|
||||
http_request=request,
|
||||
destination=source.slo_url,
|
||||
name_id=saml_session.name_id,
|
||||
name_id_format=saml_session.name_id_format,
|
||||
session_index=saml_session.session_index,
|
||||
relay_state=relay_state,
|
||||
)
|
||||
|
||||
# Insert before terminal stages (SessionEndStage, NativeLogoutStageView)
|
||||
# so the SLO redirect runs before the flow ends or the user is
|
||||
# redirected away. Provider logout stages (at index 1/2) still run
|
||||
# first since they're inserted earlier.
|
||||
if source.slo_binding == SAMLSLOBindingTypes.REDIRECT:
|
||||
redirect_url = processor.get_redirect_url()
|
||||
stage = in_memory_stage(RedirectStage, destination=redirect_url)
|
||||
else:
|
||||
# POST binding
|
||||
form_data = processor.get_post_form_data()
|
||||
executor.plan.context[PLAN_CONTEXT_TITLE] = f"Logging out of {source.name}..."
|
||||
executor.plan.context[PLAN_CONTEXT_URL] = source.slo_url
|
||||
executor.plan.context[PLAN_CONTEXT_ATTRS] = form_data
|
||||
stage = in_memory_stage(AutosubmitStageView)
|
||||
|
||||
_insert_before_terminal_stage(executor.plan, stage)
|
||||
|
||||
LOGGER.debug(
|
||||
"Injected SAML source SLO into logout flow",
|
||||
source=source.name,
|
||||
binding=source.slo_binding,
|
||||
)
|
||||
|
||||
except (KeyError, AttributeError) as exc:
|
||||
LOGGER.warning(
|
||||
"Failed to generate SAML source logout request",
|
||||
source=source.name,
|
||||
exc=exc,
|
||||
)
|
||||
|
||||
# Clean up SAMLSourceSessions for this auth session
|
||||
saml_source_sessions.delete()
|
||||
|
||||
71
authentik/sources/saml/stages.py
Normal file
71
authentik/sources/saml/stages.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""SAML Source stages and flow manager"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.core.sources.stage import PostSourceStage
|
||||
from authentik.flows.models import Flow, Stage, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
|
||||
from authentik.sources.saml.models import (
|
||||
GroupSAMLSourceConnection,
|
||||
SAMLSource,
|
||||
SAMLSourceSession,
|
||||
UserSAMLSourceConnection,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_SAML_SESSION_DATA = "saml_session_data"
|
||||
|
||||
|
||||
class SAMLPostSourceStage(PostSourceStage):
|
||||
"""Extends PostSourceStage to also create SAMLSourceSession for SLO support."""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
response = super().dispatch(request)
|
||||
|
||||
session_data = self.executor.plan.context.get(PLAN_CONTEXT_SAML_SESSION_DATA)
|
||||
if not session_data:
|
||||
return response
|
||||
|
||||
source = self.executor.plan.context.get(PLAN_CONTEXT_SOURCE)
|
||||
if not isinstance(source, SAMLSource):
|
||||
return response
|
||||
|
||||
user: User = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
|
||||
if not user or not user.pk:
|
||||
return response
|
||||
|
||||
auth_session = AuthenticatedSession.from_request(request, user)
|
||||
if not auth_session:
|
||||
return response
|
||||
|
||||
SAMLSourceSession.objects.create(
|
||||
source=source,
|
||||
user=user,
|
||||
session=auth_session,
|
||||
session_index=session_data.get("session_index", ""),
|
||||
name_id=session_data.get("name_id", ""),
|
||||
name_id_format=session_data.get("name_id_format", ""),
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Created SAMLSourceSession",
|
||||
source=source.name,
|
||||
user=user,
|
||||
session_index=session_data.get("session_index", ""),
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class SAMLSourceFlowManager(SourceFlowManager):
|
||||
"""Source flow manager for SAML Sources"""
|
||||
|
||||
user_connection_type = UserSAMLSourceConnection
|
||||
group_connection_type = GroupSAMLSourceConnection
|
||||
|
||||
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
||||
return [
|
||||
in_memory_stage(SAMLPostSourceStage),
|
||||
]
|
||||
@@ -3,7 +3,6 @@
|
||||
from urllib.parse import parse_qsl, urlparse, urlunparse
|
||||
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
@@ -13,9 +12,13 @@ from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from lxml import etree # nosec
|
||||
from structlog.stdlib import get_logger
|
||||
from xmlsec import InternalError, VerificationError
|
||||
|
||||
from authentik.common.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.common.saml.parsers.logout_request import LogoutRequestParser
|
||||
from authentik.common.saml.parsers.logout_response import LogoutResponseParser
|
||||
from authentik.flows.challenge import (
|
||||
PLAN_CONTEXT_ATTRS,
|
||||
PLAN_CONTEXT_TITLE,
|
||||
@@ -33,7 +36,7 @@ from authentik.flows.planner import (
|
||||
FlowPlan,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.stage import ChallengeStageView, RedirectStage, SessionEndStage
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.providers.saml.utils.encoding import nice64
|
||||
@@ -44,7 +47,13 @@ from authentik.sources.saml.exceptions import (
|
||||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
from authentik.sources.saml.models import (
|
||||
SAMLBindingTypes,
|
||||
SAMLSLOBindingTypes,
|
||||
SAMLSource,
|
||||
SAMLSourceSession,
|
||||
)
|
||||
from authentik.sources.saml.processors.logout_response import LogoutResponseBuilder
|
||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||
from authentik.sources.saml.processors.request import RequestProcessor
|
||||
from authentik.sources.saml.processors.response import ResponseProcessor
|
||||
@@ -52,6 +61,8 @@ from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_HEADER, ConsentS
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_SAML_RELAY_STATE = "goauthentik.io/sources/saml/relay_state"
|
||||
|
||||
|
||||
class AutosubmitStageView(ChallengeStageView):
|
||||
"""Wrapper stage to create an autosubmit challenge from plan context variables"""
|
||||
@@ -181,16 +192,195 @@ class ACSView(View):
|
||||
return bad_request_message(request, str(exc))
|
||||
|
||||
|
||||
class SLOView(LoginRequiredMixin, View):
|
||||
"""Single-Logout-View"""
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class SLOView(View):
|
||||
"""Single-Logout-View: handles SP-initiated SLO, IdP-initiated LogoutRequest,
|
||||
and LogoutResponse from IdP"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
"""Log user out and redirect them to the IdP's SLO URL."""
|
||||
def get(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
"""Handle GET requests: LogoutResponse, LogoutRequest, or initiate SLO."""
|
||||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
if not source.enabled:
|
||||
raise Http404
|
||||
logout(request)
|
||||
return redirect(source.slo_url)
|
||||
|
||||
if "SAMLResponse" in request.GET:
|
||||
return self._handle_logout_response(
|
||||
request,
|
||||
request.GET["SAMLResponse"],
|
||||
relay_state=request.GET.get("RelayState"),
|
||||
)
|
||||
|
||||
if "SAMLRequest" in request.GET:
|
||||
return self._handle_logout_request(
|
||||
request, source, request.GET["SAMLRequest"], is_post=False
|
||||
)
|
||||
|
||||
# No SAML message, initiate SP-initiated SLO
|
||||
return self._initiate_logout(request)
|
||||
|
||||
def post(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
"""Handle POST requests: LogoutResponse or LogoutRequest from the IdP."""
|
||||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
if not source.enabled:
|
||||
raise Http404
|
||||
|
||||
if "SAMLResponse" in request.POST:
|
||||
return self._handle_logout_response(
|
||||
request,
|
||||
request.POST["SAMLResponse"],
|
||||
relay_state=request.POST.get("RelayState"),
|
||||
)
|
||||
|
||||
if "SAMLRequest" in request.POST:
|
||||
return self._handle_logout_request(
|
||||
request, source, request.POST["SAMLRequest"], is_post=True
|
||||
)
|
||||
|
||||
return bad_request_message(request, "Missing SAMLRequest or SAMLResponse")
|
||||
|
||||
def _initiate_logout(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Initiate logout using the brand's invalidation flow.
|
||||
|
||||
The invalidation flow contains a UserLogoutStage which fires the
|
||||
flow_pre_user_logout signal. Our signal handler in signals.py picks that up,
|
||||
finds the SAMLSourceSession, and injects the SLO redirect/POST stage."""
|
||||
# Sources do not have an invalidation flow, use the brand's
|
||||
flow = request.brand.flow_invalidation
|
||||
if not flow:
|
||||
logout(request)
|
||||
return redirect("authentik_core:root-redirect")
|
||||
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
try:
|
||||
plan = planner.plan(request)
|
||||
except FlowNonApplicableException:
|
||||
logout(request)
|
||||
return redirect("authentik_core:root-redirect")
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
return plan.to_redirect(request, flow)
|
||||
|
||||
def _handle_logout_request(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
source: SAMLSource,
|
||||
raw_request: str,
|
||||
is_post: bool = False,
|
||||
) -> HttpResponse:
|
||||
"""Handle an incoming LogoutRequest from the IdP (IdP-initiated SLO).
|
||||
|
||||
Parses the request, deletes the SAMLSourceSession (to prevent circular
|
||||
redirect back to the IdP), runs the invalidation flow, and appends a
|
||||
final stage to send the LogoutResponse back to the IdP."""
|
||||
parser = LogoutRequestParser()
|
||||
try:
|
||||
if is_post:
|
||||
logout_request = parser.parse(raw_request)
|
||||
else:
|
||||
logout_request = parser.parse_detached(raw_request)
|
||||
except (CannotHandleAssertion, ValueError) as exc:
|
||||
LOGGER.warning("Failed to parse LogoutRequest from IdP", exc=exc)
|
||||
return bad_request_message(request, str(exc))
|
||||
|
||||
relay_state = (
|
||||
request.GET.get("RelayState") if not is_post else request.POST.get("RelayState")
|
||||
)
|
||||
|
||||
# Delete SAMLSourceSession so the source signal handler doesn't try to
|
||||
# redirect back to the IdP (which would be circular)
|
||||
SAMLSourceSession.objects.filter(
|
||||
source=source,
|
||||
user=request.user,
|
||||
).delete()
|
||||
|
||||
# Build the LogoutResponse to send back to the IdP after logout
|
||||
response_builder = LogoutResponseBuilder(
|
||||
source=source,
|
||||
http_request=request,
|
||||
destination=source.slo_url,
|
||||
in_response_to=logout_request.id,
|
||||
)
|
||||
|
||||
# Sources do not have an invalidation flow, use the brand's
|
||||
flow = request.brand.flow_invalidation
|
||||
if not flow:
|
||||
logout(request)
|
||||
return self._send_logout_response(response_builder, relay_state)
|
||||
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
try:
|
||||
plan = planner.plan(request)
|
||||
except FlowNonApplicableException:
|
||||
logout(request)
|
||||
return self._send_logout_response(response_builder, relay_state)
|
||||
|
||||
# Append logout response stage, then session end
|
||||
self._append_response_stage(plan, source, response_builder, relay_state)
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
return plan.to_redirect(request, flow)
|
||||
|
||||
def _send_logout_response(
|
||||
self,
|
||||
response_builder: LogoutResponseBuilder,
|
||||
relay_state: str | None = None,
|
||||
) -> HttpResponse:
|
||||
"""Send LogoutResponse back to the IdP directly (no flow).
|
||||
Without a flow we can't render an autosubmit form, so always redirect."""
|
||||
return redirect(response_builder.get_redirect_url(relay_state))
|
||||
|
||||
def _append_response_stage(
|
||||
self,
|
||||
plan: FlowPlan,
|
||||
source: SAMLSource,
|
||||
response_builder: LogoutResponseBuilder,
|
||||
relay_state: str | None = None,
|
||||
):
|
||||
"""Append a stage to send the LogoutResponse back to the IdP."""
|
||||
if source.slo_binding == SAMLSLOBindingTypes.REDIRECT:
|
||||
redirect_url = response_builder.get_redirect_url(relay_state)
|
||||
plan.append_stage(in_memory_stage(RedirectStage, destination=redirect_url))
|
||||
else:
|
||||
# POST binding — use autosubmit form
|
||||
form_data = response_builder.get_post_form_data(relay_state)
|
||||
plan.context[PLAN_CONTEXT_TITLE] = f"Logging out of {source.name}..."
|
||||
plan.context[PLAN_CONTEXT_URL] = source.slo_url
|
||||
plan.context[PLAN_CONTEXT_ATTRS] = form_data
|
||||
plan.append_stage(in_memory_stage(AutosubmitStageView))
|
||||
|
||||
def _handle_logout_response(
|
||||
self, request: HttpRequest, raw_response: str, relay_state: str | None = None
|
||||
) -> HttpResponse:
|
||||
"""Parse and handle a LogoutResponse from the IdP."""
|
||||
processor = LogoutResponseParser(raw_response)
|
||||
try:
|
||||
processor.parse()
|
||||
except (ValueError, etree.XMLSyntaxError) as exc:
|
||||
LOGGER.warning("Failed to parse LogoutResponse", exc=exc)
|
||||
return redirect("authentik_core:root-redirect")
|
||||
|
||||
processor.verify_status()
|
||||
|
||||
# If a RelayState was provided (e.g. the flow executor URL), advance
|
||||
# past the current stage (RedirectStage) in the plan so the flow
|
||||
# continues to the next stage instead of looping. Only redirect to the
|
||||
# value stashed in the plan context on outbound — never to the value
|
||||
# echoed back in the request, which is attacker-controllable.
|
||||
if relay_state and SESSION_KEY_PLAN in request.session:
|
||||
plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
stored_relay_state = plan.context.get(PLAN_CONTEXT_SAML_RELAY_STATE, "")
|
||||
if relay_state != stored_relay_state:
|
||||
LOGGER.warning(
|
||||
"SAML logout relay_state mismatch, possible open redirect attempt",
|
||||
received_relay_state=relay_state,
|
||||
stored_relay_state=stored_relay_state,
|
||||
)
|
||||
if plan.bindings:
|
||||
plan.pop()
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
if stored_relay_state:
|
||||
return redirect(stored_relay_state)
|
||||
return redirect("authentik_core:root-redirect")
|
||||
|
||||
|
||||
class MetadataView(View):
|
||||
|
||||
@@ -6049,18 +6049,22 @@
|
||||
"authentik_sources_saml.add_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.add_samlsource",
|
||||
"authentik_sources_saml.add_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.add_samlsourcesession",
|
||||
"authentik_sources_saml.add_usersamlsourceconnection",
|
||||
"authentik_sources_saml.change_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.change_samlsource",
|
||||
"authentik_sources_saml.change_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.change_samlsourcesession",
|
||||
"authentik_sources_saml.change_usersamlsourceconnection",
|
||||
"authentik_sources_saml.delete_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.delete_samlsource",
|
||||
"authentik_sources_saml.delete_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.delete_samlsourcesession",
|
||||
"authentik_sources_saml.delete_usersamlsourceconnection",
|
||||
"authentik_sources_saml.view_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.view_samlsource",
|
||||
"authentik_sources_saml.view_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.view_samlsourcesession",
|
||||
"authentik_sources_saml.view_usersamlsourceconnection",
|
||||
"authentik_sources_scim.add_scimsource",
|
||||
"authentik_sources_scim.add_scimsourcegroup",
|
||||
@@ -11746,18 +11750,22 @@
|
||||
"authentik_sources_saml.add_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.add_samlsource",
|
||||
"authentik_sources_saml.add_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.add_samlsourcesession",
|
||||
"authentik_sources_saml.add_usersamlsourceconnection",
|
||||
"authentik_sources_saml.change_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.change_samlsource",
|
||||
"authentik_sources_saml.change_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.change_samlsourcesession",
|
||||
"authentik_sources_saml.change_usersamlsourceconnection",
|
||||
"authentik_sources_saml.delete_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.delete_samlsource",
|
||||
"authentik_sources_saml.delete_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.delete_samlsourcesession",
|
||||
"authentik_sources_saml.delete_usersamlsourceconnection",
|
||||
"authentik_sources_saml.view_groupsamlsourceconnection",
|
||||
"authentik_sources_saml.view_samlsource",
|
||||
"authentik_sources_saml.view_samlsourcepropertymapping",
|
||||
"authentik_sources_saml.view_samlsourcesession",
|
||||
"authentik_sources_saml.view_usersamlsourceconnection",
|
||||
"authentik_sources_scim.add_scimsource",
|
||||
"authentik_sources_scim.add_scimsourcegroup",
|
||||
@@ -13578,6 +13586,15 @@
|
||||
],
|
||||
"title": "Binding type"
|
||||
},
|
||||
"slo_binding": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"REDIRECT",
|
||||
"POST"
|
||||
],
|
||||
"title": "SLO Binding",
|
||||
"description": "Binding type for Single Logout requests to the IdP."
|
||||
},
|
||||
"verification_kp": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
@@ -13634,6 +13651,16 @@
|
||||
"signed_response": {
|
||||
"type": "boolean",
|
||||
"title": "Signed response"
|
||||
},
|
||||
"sign_authn_request": {
|
||||
"type": "boolean",
|
||||
"title": "Sign AuthnRequest",
|
||||
"description": "Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set."
|
||||
},
|
||||
"sign_logout_request": {
|
||||
"type": "boolean",
|
||||
"title": "Sign LogoutRequest",
|
||||
"description": "Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
||||
16
packages/client-ts/src/apis/SourcesApi.ts
generated
16
packages/client-ts/src/apis/SourcesApi.ts
generated
@@ -101,6 +101,7 @@ import type {
|
||||
SCIMSourceUser,
|
||||
SCIMSourceUserRequest,
|
||||
SignatureAlgorithmEnum,
|
||||
SloBindingEnum,
|
||||
Source,
|
||||
SourceType,
|
||||
SyncStatus,
|
||||
@@ -749,10 +750,13 @@ export interface SourcesSamlListRequest {
|
||||
policyEngineMode?: PolicyEngineMode;
|
||||
preAuthenticationFlow?: string;
|
||||
search?: string;
|
||||
signAuthnRequest?: boolean;
|
||||
signLogoutRequest?: boolean;
|
||||
signatureAlgorithm?: SignatureAlgorithmEnum;
|
||||
signedAssertion?: boolean;
|
||||
signedResponse?: boolean;
|
||||
signingKp?: string;
|
||||
sloBinding?: SloBindingEnum;
|
||||
sloUrl?: string;
|
||||
slug?: string;
|
||||
ssoUrl?: string;
|
||||
@@ -7734,6 +7738,14 @@ export class SourcesApi extends runtime.BaseAPI {
|
||||
queryParameters["search"] = requestParameters["search"];
|
||||
}
|
||||
|
||||
if (requestParameters["signAuthnRequest"] != null) {
|
||||
queryParameters["sign_authn_request"] = requestParameters["signAuthnRequest"];
|
||||
}
|
||||
|
||||
if (requestParameters["signLogoutRequest"] != null) {
|
||||
queryParameters["sign_logout_request"] = requestParameters["signLogoutRequest"];
|
||||
}
|
||||
|
||||
if (requestParameters["signatureAlgorithm"] != null) {
|
||||
queryParameters["signature_algorithm"] = requestParameters["signatureAlgorithm"];
|
||||
}
|
||||
@@ -7750,6 +7762,10 @@ export class SourcesApi extends runtime.BaseAPI {
|
||||
queryParameters["signing_kp"] = requestParameters["signingKp"];
|
||||
}
|
||||
|
||||
if (requestParameters["sloBinding"] != null) {
|
||||
queryParameters["slo_binding"] = requestParameters["sloBinding"];
|
||||
}
|
||||
|
||||
if (requestParameters["sloUrl"] != null) {
|
||||
queryParameters["slo_url"] = requestParameters["sloUrl"];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
SignatureAlgorithmEnumFromJSON,
|
||||
SignatureAlgorithmEnumToJSON,
|
||||
} from "./SignatureAlgorithmEnum";
|
||||
import type { SloBindingEnum } from "./SloBindingEnum";
|
||||
import { SloBindingEnumFromJSON, SloBindingEnumToJSON } from "./SloBindingEnum";
|
||||
import type { UserMatchingModeEnum } from "./UserMatchingModeEnum";
|
||||
import { UserMatchingModeEnumFromJSON, UserMatchingModeEnumToJSON } from "./UserMatchingModeEnum";
|
||||
|
||||
@@ -165,6 +167,12 @@ export interface PatchedSAMLSourceRequest {
|
||||
* @memberof PatchedSAMLSourceRequest
|
||||
*/
|
||||
bindingType?: BindingTypeEnum;
|
||||
/**
|
||||
* Binding type for Single Logout requests to the IdP.
|
||||
* @type {SloBindingEnum}
|
||||
* @memberof PatchedSAMLSourceRequest
|
||||
*/
|
||||
sloBinding?: SloBindingEnum;
|
||||
/**
|
||||
* When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.
|
||||
* @type {string}
|
||||
@@ -213,6 +221,18 @@ export interface PatchedSAMLSourceRequest {
|
||||
* @memberof PatchedSAMLSourceRequest
|
||||
*/
|
||||
signedResponse?: boolean;
|
||||
/**
|
||||
* Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set.
|
||||
* @type {boolean}
|
||||
* @memberof PatchedSAMLSourceRequest
|
||||
*/
|
||||
signAuthnRequest?: boolean;
|
||||
/**
|
||||
* Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set.
|
||||
* @type {boolean}
|
||||
* @memberof PatchedSAMLSourceRequest
|
||||
*/
|
||||
signLogoutRequest?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,6 +298,8 @@ export function PatchedSAMLSourceRequestFromJSONTyped(
|
||||
json["binding_type"] == null
|
||||
? undefined
|
||||
: BindingTypeEnumFromJSON(json["binding_type"]),
|
||||
sloBinding:
|
||||
json["slo_binding"] == null ? undefined : SloBindingEnumFromJSON(json["slo_binding"]),
|
||||
verificationKp: json["verification_kp"] == null ? undefined : json["verification_kp"],
|
||||
signingKp: json["signing_kp"] == null ? undefined : json["signing_kp"],
|
||||
digestAlgorithm:
|
||||
@@ -295,6 +317,10 @@ export function PatchedSAMLSourceRequestFromJSONTyped(
|
||||
encryptionKp: json["encryption_kp"] == null ? undefined : json["encryption_kp"],
|
||||
signedAssertion: json["signed_assertion"] == null ? undefined : json["signed_assertion"],
|
||||
signedResponse: json["signed_response"] == null ? undefined : json["signed_response"],
|
||||
signAuthnRequest:
|
||||
json["sign_authn_request"] == null ? undefined : json["sign_authn_request"],
|
||||
signLogoutRequest:
|
||||
json["sign_logout_request"] == null ? undefined : json["sign_logout_request"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,6 +358,7 @@ export function PatchedSAMLSourceRequestToJSONTyped(
|
||||
force_authn: value["forceAuthn"],
|
||||
name_id_policy: SAMLNameIDPolicyEnumToJSON(value["nameIdPolicy"]),
|
||||
binding_type: BindingTypeEnumToJSON(value["bindingType"]),
|
||||
slo_binding: SloBindingEnumToJSON(value["sloBinding"]),
|
||||
verification_kp: value["verificationKp"],
|
||||
signing_kp: value["signingKp"],
|
||||
digest_algorithm: DigestAlgorithmEnumToJSON(value["digestAlgorithm"]),
|
||||
@@ -340,5 +367,7 @@ export function PatchedSAMLSourceRequestToJSONTyped(
|
||||
encryption_kp: value["encryptionKp"],
|
||||
signed_assertion: value["signedAssertion"],
|
||||
signed_response: value["signedResponse"],
|
||||
sign_authn_request: value["signAuthnRequest"],
|
||||
sign_logout_request: value["signLogoutRequest"],
|
||||
};
|
||||
}
|
||||
|
||||
29
packages/client-ts/src/models/SAMLSource.ts
generated
29
packages/client-ts/src/models/SAMLSource.ts
generated
@@ -30,6 +30,8 @@ import {
|
||||
SignatureAlgorithmEnumFromJSON,
|
||||
SignatureAlgorithmEnumToJSON,
|
||||
} from "./SignatureAlgorithmEnum";
|
||||
import type { SloBindingEnum } from "./SloBindingEnum";
|
||||
import { SloBindingEnumFromJSON, SloBindingEnumToJSON } from "./SloBindingEnum";
|
||||
import type { ThemedUrls } from "./ThemedUrls";
|
||||
import { ThemedUrlsFromJSON } from "./ThemedUrls";
|
||||
import type { UserMatchingModeEnum } from "./UserMatchingModeEnum";
|
||||
@@ -215,6 +217,12 @@ export interface SAMLSource {
|
||||
* @memberof SAMLSource
|
||||
*/
|
||||
bindingType?: BindingTypeEnum;
|
||||
/**
|
||||
* Binding type for Single Logout requests to the IdP.
|
||||
* @type {SloBindingEnum}
|
||||
* @memberof SAMLSource
|
||||
*/
|
||||
sloBinding?: SloBindingEnum;
|
||||
/**
|
||||
* When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.
|
||||
* @type {string}
|
||||
@@ -263,6 +271,18 @@ export interface SAMLSource {
|
||||
* @memberof SAMLSource
|
||||
*/
|
||||
signedResponse?: boolean;
|
||||
/**
|
||||
* Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set.
|
||||
* @type {boolean}
|
||||
* @memberof SAMLSource
|
||||
*/
|
||||
signAuthnRequest?: boolean;
|
||||
/**
|
||||
* Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set.
|
||||
* @type {boolean}
|
||||
* @memberof SAMLSource
|
||||
*/
|
||||
signLogoutRequest?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,6 +363,8 @@ export function SAMLSourceFromJSONTyped(json: any, ignoreDiscriminator: boolean)
|
||||
json["binding_type"] == null
|
||||
? undefined
|
||||
: BindingTypeEnumFromJSON(json["binding_type"]),
|
||||
sloBinding:
|
||||
json["slo_binding"] == null ? undefined : SloBindingEnumFromJSON(json["slo_binding"]),
|
||||
verificationKp: json["verification_kp"] == null ? undefined : json["verification_kp"],
|
||||
signingKp: json["signing_kp"] == null ? undefined : json["signing_kp"],
|
||||
digestAlgorithm:
|
||||
@@ -360,6 +382,10 @@ export function SAMLSourceFromJSONTyped(json: any, ignoreDiscriminator: boolean)
|
||||
encryptionKp: json["encryption_kp"] == null ? undefined : json["encryption_kp"],
|
||||
signedAssertion: json["signed_assertion"] == null ? undefined : json["signed_assertion"],
|
||||
signedResponse: json["signed_response"] == null ? undefined : json["signed_response"],
|
||||
signAuthnRequest:
|
||||
json["sign_authn_request"] == null ? undefined : json["sign_authn_request"],
|
||||
signLogoutRequest:
|
||||
json["sign_logout_request"] == null ? undefined : json["sign_logout_request"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -407,6 +433,7 @@ export function SAMLSourceToJSONTyped(
|
||||
force_authn: value["forceAuthn"],
|
||||
name_id_policy: SAMLNameIDPolicyEnumToJSON(value["nameIdPolicy"]),
|
||||
binding_type: BindingTypeEnumToJSON(value["bindingType"]),
|
||||
slo_binding: SloBindingEnumToJSON(value["sloBinding"]),
|
||||
verification_kp: value["verificationKp"],
|
||||
signing_kp: value["signingKp"],
|
||||
digest_algorithm: DigestAlgorithmEnumToJSON(value["digestAlgorithm"]),
|
||||
@@ -415,5 +442,7 @@ export function SAMLSourceToJSONTyped(
|
||||
encryption_kp: value["encryptionKp"],
|
||||
signed_assertion: value["signedAssertion"],
|
||||
signed_response: value["signedResponse"],
|
||||
sign_authn_request: value["signAuthnRequest"],
|
||||
sign_logout_request: value["signLogoutRequest"],
|
||||
};
|
||||
}
|
||||
|
||||
29
packages/client-ts/src/models/SAMLSourceRequest.ts
generated
29
packages/client-ts/src/models/SAMLSourceRequest.ts
generated
@@ -30,6 +30,8 @@ import {
|
||||
SignatureAlgorithmEnumFromJSON,
|
||||
SignatureAlgorithmEnumToJSON,
|
||||
} from "./SignatureAlgorithmEnum";
|
||||
import type { SloBindingEnum } from "./SloBindingEnum";
|
||||
import { SloBindingEnumFromJSON, SloBindingEnumToJSON } from "./SloBindingEnum";
|
||||
import type { UserMatchingModeEnum } from "./UserMatchingModeEnum";
|
||||
import { UserMatchingModeEnumFromJSON, UserMatchingModeEnumToJSON } from "./UserMatchingModeEnum";
|
||||
|
||||
@@ -165,6 +167,12 @@ export interface SAMLSourceRequest {
|
||||
* @memberof SAMLSourceRequest
|
||||
*/
|
||||
bindingType?: BindingTypeEnum;
|
||||
/**
|
||||
* Binding type for Single Logout requests to the IdP.
|
||||
* @type {SloBindingEnum}
|
||||
* @memberof SAMLSourceRequest
|
||||
*/
|
||||
sloBinding?: SloBindingEnum;
|
||||
/**
|
||||
* When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.
|
||||
* @type {string}
|
||||
@@ -213,6 +221,18 @@ export interface SAMLSourceRequest {
|
||||
* @memberof SAMLSourceRequest
|
||||
*/
|
||||
signedResponse?: boolean;
|
||||
/**
|
||||
* Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set.
|
||||
* @type {boolean}
|
||||
* @memberof SAMLSourceRequest
|
||||
*/
|
||||
signAuthnRequest?: boolean;
|
||||
/**
|
||||
* Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set.
|
||||
* @type {boolean}
|
||||
* @memberof SAMLSourceRequest
|
||||
*/
|
||||
signLogoutRequest?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,6 +300,8 @@ export function SAMLSourceRequestFromJSONTyped(
|
||||
json["binding_type"] == null
|
||||
? undefined
|
||||
: BindingTypeEnumFromJSON(json["binding_type"]),
|
||||
sloBinding:
|
||||
json["slo_binding"] == null ? undefined : SloBindingEnumFromJSON(json["slo_binding"]),
|
||||
verificationKp: json["verification_kp"] == null ? undefined : json["verification_kp"],
|
||||
signingKp: json["signing_kp"] == null ? undefined : json["signing_kp"],
|
||||
digestAlgorithm:
|
||||
@@ -297,6 +319,10 @@ export function SAMLSourceRequestFromJSONTyped(
|
||||
encryptionKp: json["encryption_kp"] == null ? undefined : json["encryption_kp"],
|
||||
signedAssertion: json["signed_assertion"] == null ? undefined : json["signed_assertion"],
|
||||
signedResponse: json["signed_response"] == null ? undefined : json["signed_response"],
|
||||
signAuthnRequest:
|
||||
json["sign_authn_request"] == null ? undefined : json["sign_authn_request"],
|
||||
signLogoutRequest:
|
||||
json["sign_logout_request"] == null ? undefined : json["sign_logout_request"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -334,6 +360,7 @@ export function SAMLSourceRequestToJSONTyped(
|
||||
force_authn: value["forceAuthn"],
|
||||
name_id_policy: SAMLNameIDPolicyEnumToJSON(value["nameIdPolicy"]),
|
||||
binding_type: BindingTypeEnumToJSON(value["bindingType"]),
|
||||
slo_binding: SloBindingEnumToJSON(value["sloBinding"]),
|
||||
verification_kp: value["verificationKp"],
|
||||
signing_kp: value["signingKp"],
|
||||
digest_algorithm: DigestAlgorithmEnumToJSON(value["digestAlgorithm"]),
|
||||
@@ -342,5 +369,7 @@ export function SAMLSourceRequestToJSONTyped(
|
||||
encryption_kp: value["encryptionKp"],
|
||||
signed_assertion: value["signedAssertion"],
|
||||
signed_response: value["signedResponse"],
|
||||
sign_authn_request: value["signAuthnRequest"],
|
||||
sign_logout_request: value["signLogoutRequest"],
|
||||
};
|
||||
}
|
||||
|
||||
57
packages/client-ts/src/models/SloBindingEnum.ts
generated
Normal file
57
packages/client-ts/src/models/SloBindingEnum.ts
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.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 SloBindingEnum = {
|
||||
Redirect: "REDIRECT",
|
||||
Post: "POST",
|
||||
UnknownDefaultOpenApi: "11184809",
|
||||
} as const;
|
||||
export type SloBindingEnum = (typeof SloBindingEnum)[keyof typeof SloBindingEnum];
|
||||
|
||||
export function instanceOfSloBindingEnum(value: any): boolean {
|
||||
for (const key in SloBindingEnum) {
|
||||
if (Object.prototype.hasOwnProperty.call(SloBindingEnum, key)) {
|
||||
if (SloBindingEnum[key as keyof typeof SloBindingEnum] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function SloBindingEnumFromJSON(json: any): SloBindingEnum {
|
||||
return SloBindingEnumFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function SloBindingEnumFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): SloBindingEnum {
|
||||
return json as SloBindingEnum;
|
||||
}
|
||||
|
||||
export function SloBindingEnumToJSON(value?: SloBindingEnum | null): any {
|
||||
return value as any;
|
||||
}
|
||||
|
||||
export function SloBindingEnumToJSONTyped(
|
||||
value: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): SloBindingEnum {
|
||||
return value as SloBindingEnum;
|
||||
}
|
||||
1
packages/client-ts/src/models/index.ts
generated
1
packages/client-ts/src/models/index.ts
generated
@@ -766,6 +766,7 @@ export * from "./SettingsRequest";
|
||||
export * from "./SeverityEnum";
|
||||
export * from "./ShellChallenge";
|
||||
export * from "./SignatureAlgorithmEnum";
|
||||
export * from "./SloBindingEnum";
|
||||
export * from "./Software";
|
||||
export * from "./SoftwareRequest";
|
||||
export * from "./Source";
|
||||
|
||||
@@ -192,6 +192,7 @@ ignore_missing_imports = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"authentik.common.*",
|
||||
"authentik.admin.*",
|
||||
"authentik.api.*",
|
||||
"authentik.blueprints.*",
|
||||
|
||||
63
schema.yml
63
schema.yml
@@ -24369,6 +24369,14 @@ paths:
|
||||
type: string
|
||||
format: uuid
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
- in: query
|
||||
name: sign_authn_request
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: sign_logout_request
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: signature_algorithm
|
||||
schema:
|
||||
@@ -24386,6 +24394,14 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: slo_binding
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SloBindingEnum'
|
||||
description: |+
|
||||
Binding type for Single Logout requests to the IdP.
|
||||
|
||||
- in: query
|
||||
name: slo_url
|
||||
schema:
|
||||
@@ -50436,6 +50452,10 @@ components:
|
||||
no Policy is sent.
|
||||
binding_type:
|
||||
$ref: '#/components/schemas/BindingTypeEnum'
|
||||
slo_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SloBindingEnum'
|
||||
description: Binding type for Single Logout requests to the IdP.
|
||||
verification_kp:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -50473,6 +50493,16 @@ components:
|
||||
type: boolean
|
||||
signed_response:
|
||||
type: boolean
|
||||
sign_authn_request:
|
||||
type: boolean
|
||||
title: Sign AuthnRequest
|
||||
description: Whether to sign outgoing AuthnRequests. Requires a Signing
|
||||
Keypair to be set.
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
title: Sign LogoutRequest
|
||||
description: Whether to sign outgoing LogoutRequests. Requires a Signing
|
||||
Keypair to be set.
|
||||
PatchedSCIMMappingRequest:
|
||||
type: object
|
||||
description: SCIMMapping Serializer
|
||||
@@ -54176,6 +54206,10 @@ components:
|
||||
no Policy is sent.
|
||||
binding_type:
|
||||
$ref: '#/components/schemas/BindingTypeEnum'
|
||||
slo_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SloBindingEnum'
|
||||
description: Binding type for Single Logout requests to the IdP.
|
||||
verification_kp:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -54212,6 +54246,16 @@ components:
|
||||
type: boolean
|
||||
signed_response:
|
||||
type: boolean
|
||||
sign_authn_request:
|
||||
type: boolean
|
||||
title: Sign AuthnRequest
|
||||
description: Whether to sign outgoing AuthnRequests. Requires a Signing
|
||||
Keypair to be set.
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
title: Sign LogoutRequest
|
||||
description: Whether to sign outgoing LogoutRequests. Requires a Signing
|
||||
Keypair to be set.
|
||||
required:
|
||||
- component
|
||||
- icon_themed_urls
|
||||
@@ -54380,6 +54424,10 @@ components:
|
||||
no Policy is sent.
|
||||
binding_type:
|
||||
$ref: '#/components/schemas/BindingTypeEnum'
|
||||
slo_binding:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SloBindingEnum'
|
||||
description: Binding type for Single Logout requests to the IdP.
|
||||
verification_kp:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -54417,6 +54465,16 @@ components:
|
||||
type: boolean
|
||||
signed_response:
|
||||
type: boolean
|
||||
sign_authn_request:
|
||||
type: boolean
|
||||
title: Sign AuthnRequest
|
||||
description: Whether to sign outgoing AuthnRequests. Requires a Signing
|
||||
Keypair to be set.
|
||||
sign_logout_request:
|
||||
type: boolean
|
||||
title: Sign LogoutRequest
|
||||
description: Whether to sign outgoing LogoutRequests. Requires a Signing
|
||||
Keypair to be set.
|
||||
required:
|
||||
- name
|
||||
- pre_authentication_flow
|
||||
@@ -55677,6 +55735,11 @@ components:
|
||||
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
|
||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||
type: string
|
||||
SloBindingEnum:
|
||||
enum:
|
||||
- REDIRECT
|
||||
- POST
|
||||
type: string
|
||||
Software:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
SAMLNameIDPolicyEnum,
|
||||
SAMLSource,
|
||||
SignatureAlgorithmEnum,
|
||||
SloBindingEnum,
|
||||
SourcesApi,
|
||||
UsageEnum,
|
||||
UserMatchingModeEnum,
|
||||
@@ -226,6 +227,32 @@ export class SAMLSourceForm extends BaseSourceForm<SAMLSource> {
|
||||
${msg("Optional URL if the IDP supports Single-Logout.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("SLO Binding")}
|
||||
required
|
||||
name="sloBinding"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Redirect binding"),
|
||||
value: SloBindingEnum.Redirect,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Post binding"),
|
||||
value: SloBindingEnum.Post,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.sloBinding}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Binding type used for sending Single Logout requests to the IdP.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Issuer")} name="issuer">
|
||||
<input
|
||||
type="text"
|
||||
@@ -274,6 +301,22 @@ export class SAMLSourceForm extends BaseSourceForm<SAMLSource> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-switch-input
|
||||
name="signAuthnRequest"
|
||||
label=${msg("Sign AuthnRequest")}
|
||||
?checked=${this.instance?.signAuthnRequest ?? false}
|
||||
help=${msg(
|
||||
"Whether to sign outgoing AuthnRequests. Requires a Signing Keypair to be set.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="signLogoutRequest"
|
||||
label=${msg("Sign LogoutRequest")}
|
||||
?checked=${this.instance?.signLogoutRequest ?? false}
|
||||
help=${msg(
|
||||
"Whether to sign outgoing LogoutRequests. Requires a Signing Keypair to be set.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
|
||||
Reference in New Issue
Block a user