authentik.providers.saml.native_logout

SAML Logout stages for automatic injection

 1"""SAML Logout stages for automatic injection"""
 2
 3from django.http import HttpResponse
 4from rest_framework.fields import BooleanField, CharField, ChoiceField
 5from structlog.stdlib import get_logger
 6
 7from authentik.flows.challenge import Challenge, ChallengeResponse, HttpChallengeResponse
 8from authentik.flows.stage import ChallengeStageView
 9from authentik.providers.saml.models import SAMLBindings
10from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS
11
12LOGGER = get_logger()
13
14
15class NativeLogoutStageViewBase(ChallengeStageView):
16    """Base class for native browser logout stages with shared functionality"""
17
18
19class NativeLogoutChallenge(Challenge):
20    """Challenge for native browser logout"""
21
22    component = CharField(default="ak-provider-saml-native-logout")
23    provider_name = CharField(required=False)
24    is_complete = BooleanField(required=False, default=False)
25
26    post_url = CharField(required=False)
27    redirect_url = CharField(required=False)
28
29    saml_binding = ChoiceField(choices=SAMLBindings.choices, required=False)
30    saml_request = CharField(required=False)
31    saml_response = CharField(required=False)
32    saml_relay_state = CharField(required=False)
33
34
35class NativeLogoutChallengeResponse(ChallengeResponse):
36    """Response for native browser logout"""
37
38    component = CharField(default="ak-provider-saml-native-logout")
39
40
41class NativeLogoutStageView(NativeLogoutStageViewBase):
42    """Native browser logout stage that handles redirect chain and post logouts."""
43
44    response_class = NativeLogoutChallengeResponse
45
46    def get_challenge(self, *args, **kwargs) -> Challenge:
47        """Generate challenge for next provider"""
48        pending = self.executor.plan.context.get(PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS, [])
49        if not pending:
50            self.executor.plan.context.pop(PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS, None)
51            return NativeLogoutChallenge(
52                data={
53                    "component": "ak-provider-saml-native-logout",
54                    "is_complete": True,
55                }
56            )
57
58        logout_data = pending.pop(0)
59        self.executor.plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = pending
60
61        return NativeLogoutChallenge(
62            data={
63                "component": "ak-provider-saml-native-logout",
64                **logout_data,
65            }
66        )
67
68    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
69        """Challenge completed"""
70        challenge = self.get_challenge()
71
72        if not challenge.is_valid():
73            return self.executor.stage_invalid()
74
75        if challenge.initial_data.get("is_complete"):
76            return self.executor.stage_ok()
77
78        return HttpChallengeResponse(challenge)
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class NativeLogoutStageViewBase(authentik.flows.stage.ChallengeStageView):
16class NativeLogoutStageViewBase(ChallengeStageView):
17    """Base class for native browser logout stages with shared functionality"""

Base class for native browser logout stages with shared functionality

class NativeLogoutChallenge(authentik.flows.challenge.Challenge):
20class NativeLogoutChallenge(Challenge):
21    """Challenge for native browser logout"""
22
23    component = CharField(default="ak-provider-saml-native-logout")
24    provider_name = CharField(required=False)
25    is_complete = BooleanField(required=False, default=False)
26
27    post_url = CharField(required=False)
28    redirect_url = CharField(required=False)
29
30    saml_binding = ChoiceField(choices=SAMLBindings.choices, required=False)
31    saml_request = CharField(required=False)
32    saml_response = CharField(required=False)
33    saml_relay_state = CharField(required=False)

Challenge for native browser logout

component
provider_name
is_complete
post_url
redirect_url
saml_binding
saml_request
saml_response
saml_relay_state
class NativeLogoutChallengeResponse(authentik.flows.challenge.ChallengeResponse):
36class NativeLogoutChallengeResponse(ChallengeResponse):
37    """Response for native browser logout"""
38
39    component = CharField(default="ak-provider-saml-native-logout")

Response for native browser logout

component
class NativeLogoutStageView(NativeLogoutStageViewBase):
42class NativeLogoutStageView(NativeLogoutStageViewBase):
43    """Native browser logout stage that handles redirect chain and post logouts."""
44
45    response_class = NativeLogoutChallengeResponse
46
47    def get_challenge(self, *args, **kwargs) -> Challenge:
48        """Generate challenge for next provider"""
49        pending = self.executor.plan.context.get(PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS, [])
50        if not pending:
51            self.executor.plan.context.pop(PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS, None)
52            return NativeLogoutChallenge(
53                data={
54                    "component": "ak-provider-saml-native-logout",
55                    "is_complete": True,
56                }
57            )
58
59        logout_data = pending.pop(0)
60        self.executor.plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = pending
61
62        return NativeLogoutChallenge(
63            data={
64                "component": "ak-provider-saml-native-logout",
65                **logout_data,
66            }
67        )
68
69    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
70        """Challenge completed"""
71        challenge = self.get_challenge()
72
73        if not challenge.is_valid():
74            return self.executor.stage_invalid()
75
76        if challenge.initial_data.get("is_complete"):
77            return self.executor.stage_ok()
78
79        return HttpChallengeResponse(challenge)

Native browser logout stage that handles redirect chain and post logouts.

response_class = <class 'NativeLogoutChallengeResponse'>
def get_challenge(self, *args, **kwargs) -> authentik.flows.challenge.Challenge:
47    def get_challenge(self, *args, **kwargs) -> Challenge:
48        """Generate challenge for next provider"""
49        pending = self.executor.plan.context.get(PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS, [])
50        if not pending:
51            self.executor.plan.context.pop(PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS, None)
52            return NativeLogoutChallenge(
53                data={
54                    "component": "ak-provider-saml-native-logout",
55                    "is_complete": True,
56                }
57            )
58
59        logout_data = pending.pop(0)
60        self.executor.plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = pending
61
62        return NativeLogoutChallenge(
63            data={
64                "component": "ak-provider-saml-native-logout",
65                **logout_data,
66            }
67        )

Generate challenge for next provider

def challenge_valid( self, response: authentik.flows.challenge.ChallengeResponse) -> django.http.response.HttpResponse:
69    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
70        """Challenge completed"""
71        challenge = self.get_challenge()
72
73        if not challenge.is_valid():
74            return self.executor.stage_invalid()
75
76        if challenge.initial_data.get("is_complete"):
77            return self.executor.stage_ok()
78
79        return HttpChallengeResponse(challenge)

Challenge completed