authentik.flows.challenge

Challenge helpers

  1"""Challenge helpers"""
  2
  3from dataclasses import asdict, is_dataclass
  4from enum import Enum
  5from typing import TYPE_CHECKING, TypedDict
  6from uuid import UUID
  7
  8from django.core.serializers.json import DjangoJSONEncoder
  9from django.db import models
 10from django.http import JsonResponse
 11from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField
 12from rest_framework.request import Request
 13
 14from authentik.core.api.utils import PassiveSerializer, ThemedUrlsSerializer
 15from authentik.lib.utils.errors import exception_to_string
 16
 17if TYPE_CHECKING:
 18    from authentik.flows.stage import StageView
 19
 20PLAN_CONTEXT_TITLE = "title"
 21PLAN_CONTEXT_URL = "url"
 22PLAN_CONTEXT_ATTRS = "attrs"
 23
 24
 25class FlowLayout(models.TextChoices):
 26    """Flow layouts"""
 27
 28    STACKED = "stacked"
 29    CONTENT_LEFT = "content_left"
 30    CONTENT_RIGHT = "content_right"
 31    SIDEBAR_LEFT = "sidebar_left"
 32    SIDEBAR_RIGHT = "sidebar_right"
 33
 34    SIDEBAR_LEFT_FRAME_BACKGROUND = "sidebar_left_frame_background"
 35    SIDEBAR_RIGHT_FRAME_BACKGROUND = "sidebar_right_frame_background"
 36
 37
 38class ErrorDetailSerializer(PassiveSerializer):
 39    """Serializer for rest_framework's error messages"""
 40
 41    string = CharField()
 42    code = CharField()
 43
 44
 45class ContextualFlowInfo(PassiveSerializer):
 46    """Contextual flow information for a challenge"""
 47
 48    title = CharField(required=False, allow_blank=True)
 49    background = CharField(required=False)
 50    background_themed_urls = ThemedUrlsSerializer(required=False, allow_null=True)
 51    cancel_url = CharField()
 52    layout = ChoiceField(choices=[(x.value, x.name) for x in FlowLayout])
 53
 54
 55class Challenge(PassiveSerializer):
 56    """Challenge that gets sent to the client based on which stage
 57    is currently active"""
 58
 59    flow_info = ContextualFlowInfo(required=False)
 60    component = CharField(default="")
 61
 62    response_errors = DictField(
 63        child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
 64    )
 65
 66
 67class RedirectChallenge(Challenge):
 68    """Challenge type to redirect the client"""
 69
 70    to = CharField()
 71    component = CharField(default="xak-flow-redirect")
 72
 73
 74class ShellChallenge(Challenge):
 75    """challenge type to render HTML as-is"""
 76
 77    body = CharField()
 78    component = CharField(default="xak-flow-shell")
 79
 80
 81class WithUserInfoChallenge(Challenge):
 82    """Challenge base which shows some user info"""
 83
 84    pending_user = CharField(allow_blank=True)
 85    pending_user_avatar = CharField()
 86
 87
 88class FlowErrorChallenge(Challenge):
 89    """Challenge class when an unhandled error occurs during a stage. Normal users
 90    are shown an error message, superusers are shown a full stacktrace."""
 91
 92    component = CharField(default="ak-stage-flow-error")
 93
 94    request_id = CharField()
 95
 96    error = CharField(required=False)
 97    traceback = CharField(required=False)
 98
 99    def __init__(self, request: Request | None = None, error: Exception | None = None):
100        super().__init__(data={})
101        if not request or not error:
102            return
103        self.initial_data["request_id"] = request.request_id
104        from authentik.core.models import USER_ATTRIBUTE_DEBUG
105
106        if request.user and request.user.is_authenticated:
107            if request.user.is_superuser or request.user.group_attributes(request).get(
108                USER_ATTRIBUTE_DEBUG, False
109            ):
110                self.initial_data["error"] = str(error)
111                self.initial_data["traceback"] = exception_to_string(error)
112
113
114class AccessDeniedChallenge(WithUserInfoChallenge):
115    """Challenge when a flow's active stage calls `stage_invalid()`."""
116
117    component = CharField(default="ak-stage-access-denied")
118
119    error_message = CharField(required=False)
120
121
122class SessionEndChallenge(WithUserInfoChallenge):
123    """Challenge for ending a session"""
124
125    component = CharField(default="ak-stage-session-end")
126
127    application_name = CharField(required=False)
128    application_launch_url = CharField(required=False)
129
130    invalidation_flow_url = CharField(required=False)
131    brand_name = CharField(required=True)
132
133
134class PermissionDict(TypedDict):
135    """Consent Permission"""
136
137    id: str
138    name: str
139
140
141class ChallengeResponse(PassiveSerializer):
142    """Base class for all challenge responses"""
143
144    stage: StageView | None
145    component = CharField(default="xak-flow-response-default")
146
147    def __init__(self, instance=None, data=None, **kwargs):
148        self.stage = kwargs.pop("stage", None)
149        super().__init__(instance=instance, data=data, **kwargs)
150
151
152class AutosubmitChallenge(Challenge):
153    """Autosubmit challenge used to send and navigate a POST request"""
154
155    url = CharField()
156    attrs = DictField(child=CharField(allow_blank=True), allow_empty=True)
157    title = CharField(required=False)
158    component = CharField(default="ak-stage-autosubmit")
159
160
161class AutoSubmitChallengeResponse(ChallengeResponse):
162    """Pseudo class for autosubmit response"""
163
164    component = CharField(default="ak-stage-autosubmit")
165
166
167class FrameChallenge(Challenge):
168    """Challenge type to render a frame"""
169
170    component = CharField(default="xak-flow-frame")
171    url = CharField()
172    loading_overlay = BooleanField(default=False)
173    loading_text = CharField()
174
175
176class FrameChallengeResponse(ChallengeResponse):
177
178    component = CharField(default="xak-flow-frame")
179
180
181class DataclassEncoder(DjangoJSONEncoder):
182    """Convert any dataclass to json"""
183
184    def default(self, o):
185        if is_dataclass(o):
186            return asdict(o)
187        if isinstance(o, UUID):
188            return str(o)
189        if isinstance(o, Enum):
190            return o.value
191        return super().default(o)  # pragma: no cover
192
193
194class HttpChallengeResponse(JsonResponse):
195    """Subclass of JsonResponse that uses the `DataclassEncoder`"""
196
197    def __init__(self, challenge, **kwargs) -> None:
198        super().__init__(challenge.data, encoder=DataclassEncoder, **kwargs)
PLAN_CONTEXT_TITLE = 'title'
PLAN_CONTEXT_URL = 'url'
PLAN_CONTEXT_ATTRS = 'attrs'
class FlowLayout(django.db.models.enums.TextChoices):
26class FlowLayout(models.TextChoices):
27    """Flow layouts"""
28
29    STACKED = "stacked"
30    CONTENT_LEFT = "content_left"
31    CONTENT_RIGHT = "content_right"
32    SIDEBAR_LEFT = "sidebar_left"
33    SIDEBAR_RIGHT = "sidebar_right"
34
35    SIDEBAR_LEFT_FRAME_BACKGROUND = "sidebar_left_frame_background"
36    SIDEBAR_RIGHT_FRAME_BACKGROUND = "sidebar_right_frame_background"

Flow layouts

STACKED = FlowLayout.STACKED
CONTENT_LEFT = FlowLayout.CONTENT_LEFT
CONTENT_RIGHT = FlowLayout.CONTENT_RIGHT
SIDEBAR_LEFT = FlowLayout.SIDEBAR_LEFT
SIDEBAR_RIGHT = FlowLayout.SIDEBAR_RIGHT
SIDEBAR_LEFT_FRAME_BACKGROUND = FlowLayout.SIDEBAR_LEFT_FRAME_BACKGROUND
SIDEBAR_RIGHT_FRAME_BACKGROUND = FlowLayout.SIDEBAR_RIGHT_FRAME_BACKGROUND
class ErrorDetailSerializer(authentik.core.api.utils.PassiveSerializer):
39class ErrorDetailSerializer(PassiveSerializer):
40    """Serializer for rest_framework's error messages"""
41
42    string = CharField()
43    code = CharField()

Serializer for rest_framework's error messages

string
code
class ContextualFlowInfo(authentik.core.api.utils.PassiveSerializer):
46class ContextualFlowInfo(PassiveSerializer):
47    """Contextual flow information for a challenge"""
48
49    title = CharField(required=False, allow_blank=True)
50    background = CharField(required=False)
51    background_themed_urls = ThemedUrlsSerializer(required=False, allow_null=True)
52    cancel_url = CharField()
53    layout = ChoiceField(choices=[(x.value, x.name) for x in FlowLayout])

Contextual flow information for a challenge

title
background
background_themed_urls
cancel_url
layout
class Challenge(authentik.core.api.utils.PassiveSerializer):
56class Challenge(PassiveSerializer):
57    """Challenge that gets sent to the client based on which stage
58    is currently active"""
59
60    flow_info = ContextualFlowInfo(required=False)
61    component = CharField(default="")
62
63    response_errors = DictField(
64        child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
65    )

Challenge that gets sent to the client based on which stage is currently active

flow_info
component
response_errors
class RedirectChallenge(Challenge):
68class RedirectChallenge(Challenge):
69    """Challenge type to redirect the client"""
70
71    to = CharField()
72    component = CharField(default="xak-flow-redirect")

Challenge type to redirect the client

to
component
class ShellChallenge(Challenge):
75class ShellChallenge(Challenge):
76    """challenge type to render HTML as-is"""
77
78    body = CharField()
79    component = CharField(default="xak-flow-shell")

challenge type to render HTML as-is

body
component
class WithUserInfoChallenge(Challenge):
82class WithUserInfoChallenge(Challenge):
83    """Challenge base which shows some user info"""
84
85    pending_user = CharField(allow_blank=True)
86    pending_user_avatar = CharField()

Challenge base which shows some user info

pending_user
pending_user_avatar
class FlowErrorChallenge(Challenge):
 89class FlowErrorChallenge(Challenge):
 90    """Challenge class when an unhandled error occurs during a stage. Normal users
 91    are shown an error message, superusers are shown a full stacktrace."""
 92
 93    component = CharField(default="ak-stage-flow-error")
 94
 95    request_id = CharField()
 96
 97    error = CharField(required=False)
 98    traceback = CharField(required=False)
 99
100    def __init__(self, request: Request | None = None, error: Exception | None = None):
101        super().__init__(data={})
102        if not request or not error:
103            return
104        self.initial_data["request_id"] = request.request_id
105        from authentik.core.models import USER_ATTRIBUTE_DEBUG
106
107        if request.user and request.user.is_authenticated:
108            if request.user.is_superuser or request.user.group_attributes(request).get(
109                USER_ATTRIBUTE_DEBUG, False
110            ):
111                self.initial_data["error"] = str(error)
112                self.initial_data["traceback"] = exception_to_string(error)

Challenge class when an unhandled error occurs during a stage. Normal users are shown an error message, superusers are shown a full stacktrace.

FlowErrorChallenge( request: rest_framework.request.Request | None = None, error: Exception | None = None)
100    def __init__(self, request: Request | None = None, error: Exception | None = None):
101        super().__init__(data={})
102        if not request or not error:
103            return
104        self.initial_data["request_id"] = request.request_id
105        from authentik.core.models import USER_ATTRIBUTE_DEBUG
106
107        if request.user and request.user.is_authenticated:
108            if request.user.is_superuser or request.user.group_attributes(request).get(
109                USER_ATTRIBUTE_DEBUG, False
110            ):
111                self.initial_data["error"] = str(error)
112                self.initial_data["traceback"] = exception_to_string(error)
component
request_id
error
traceback
class AccessDeniedChallenge(WithUserInfoChallenge):
115class AccessDeniedChallenge(WithUserInfoChallenge):
116    """Challenge when a flow's active stage calls `stage_invalid()`."""
117
118    component = CharField(default="ak-stage-access-denied")
119
120    error_message = CharField(required=False)

Challenge when a flow's active stage calls stage_invalid().

component
error_message
class SessionEndChallenge(WithUserInfoChallenge):
123class SessionEndChallenge(WithUserInfoChallenge):
124    """Challenge for ending a session"""
125
126    component = CharField(default="ak-stage-session-end")
127
128    application_name = CharField(required=False)
129    application_launch_url = CharField(required=False)
130
131    invalidation_flow_url = CharField(required=False)
132    brand_name = CharField(required=True)

Challenge for ending a session

component
application_name
application_launch_url
invalidation_flow_url
brand_name
class PermissionDict(typing.TypedDict):
135class PermissionDict(TypedDict):
136    """Consent Permission"""
137
138    id: str
139    name: str

Consent Permission

id: str
name: str
class ChallengeResponse(authentik.core.api.utils.PassiveSerializer):
142class ChallengeResponse(PassiveSerializer):
143    """Base class for all challenge responses"""
144
145    stage: StageView | None
146    component = CharField(default="xak-flow-response-default")
147
148    def __init__(self, instance=None, data=None, **kwargs):
149        self.stage = kwargs.pop("stage", None)
150        super().__init__(instance=instance, data=data, **kwargs)

Base class for all challenge responses

ChallengeResponse(instance=None, data=None, **kwargs)
148    def __init__(self, instance=None, data=None, **kwargs):
149        self.stage = kwargs.pop("stage", None)
150        super().__init__(instance=instance, data=data, **kwargs)
component
class AutosubmitChallenge(Challenge):
153class AutosubmitChallenge(Challenge):
154    """Autosubmit challenge used to send and navigate a POST request"""
155
156    url = CharField()
157    attrs = DictField(child=CharField(allow_blank=True), allow_empty=True)
158    title = CharField(required=False)
159    component = CharField(default="ak-stage-autosubmit")

Autosubmit challenge used to send and navigate a POST request

url
attrs
title
component
class AutoSubmitChallengeResponse(ChallengeResponse):
162class AutoSubmitChallengeResponse(ChallengeResponse):
163    """Pseudo class for autosubmit response"""
164
165    component = CharField(default="ak-stage-autosubmit")

Pseudo class for autosubmit response

component
class FrameChallenge(Challenge):
168class FrameChallenge(Challenge):
169    """Challenge type to render a frame"""
170
171    component = CharField(default="xak-flow-frame")
172    url = CharField()
173    loading_overlay = BooleanField(default=False)
174    loading_text = CharField()

Challenge type to render a frame

component
url
loading_overlay
loading_text
class FrameChallengeResponse(ChallengeResponse):
177class FrameChallengeResponse(ChallengeResponse):
178
179    component = CharField(default="xak-flow-frame")

Base class for all challenge responses

component
class DataclassEncoder(django.core.serializers.json.DjangoJSONEncoder):
182class DataclassEncoder(DjangoJSONEncoder):
183    """Convert any dataclass to json"""
184
185    def default(self, o):
186        if is_dataclass(o):
187            return asdict(o)
188        if isinstance(o, UUID):
189            return str(o)
190        if isinstance(o, Enum):
191            return o.value
192        return super().default(o)  # pragma: no cover

Convert any dataclass to json

def default(self, o):
185    def default(self, o):
186        if is_dataclass(o):
187            return asdict(o)
188        if isinstance(o, UUID):
189            return str(o)
190        if isinstance(o, Enum):
191            return o.value
192        return super().default(o)  # pragma: no cover

Implement this method in a subclass such that it returns a serializable object for o, or calls the base implementation (to raise a TypeError).

For example, to support arbitrary iterators, you could implement default like this::

def default(self, o):
    try:
        iterable = iter(o)
    except TypeError:
        pass
    else:
        return list(iterable)
    # Let the base class default method raise the TypeError
    return super().default(o)
class HttpChallengeResponse(django.http.response.JsonResponse):
195class HttpChallengeResponse(JsonResponse):
196    """Subclass of JsonResponse that uses the `DataclassEncoder`"""
197
198    def __init__(self, challenge, **kwargs) -> None:
199        super().__init__(challenge.data, encoder=DataclassEncoder, **kwargs)

Subclass of JsonResponse that uses the DataclassEncoder

HttpChallengeResponse(challenge, **kwargs)
198    def __init__(self, challenge, **kwargs) -> None:
199        super().__init__(challenge.data, encoder=DataclassEncoder, **kwargs)