diff --git a/authentik/flows/apps.py b/authentik/flows/apps.py
index 513b3f044..9fcc8c52a 100644
--- a/authentik/flows/apps.py
+++ b/authentik/flows/apps.py
@@ -2,6 +2,9 @@
from importlib import import_module
from django.apps import AppConfig
+from django.db.utils import ProgrammingError
+
+from authentik.lib.utils.reflection import all_subclasses
class AuthentikFlowsConfig(AppConfig):
@@ -14,3 +17,10 @@ class AuthentikFlowsConfig(AppConfig):
def ready(self):
import_module("authentik.flows.signals")
+ try:
+ from authentik.flows.models import Stage
+
+ for stage in all_subclasses(Stage):
+ _ = stage().type
+ except ProgrammingError:
+ pass
diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py
index f1f04a9ab..0dd8a9f3e 100644
--- a/authentik/flows/challenge.py
+++ b/authentik/flows/challenge.py
@@ -35,9 +35,9 @@ class Challenge(PassiveSerializer):
type = ChoiceField(
choices=[(x.value, x.name) for x in ChallengeTypes],
)
- component = CharField(required=False)
title = CharField(required=False)
background = CharField(required=False)
+ component = CharField(default="")
response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
@@ -48,12 +48,14 @@ class RedirectChallenge(Challenge):
"""Challenge type to redirect the client"""
to = CharField()
+ component = CharField(default="xak-flow-redirect")
class ShellChallenge(Challenge):
- """Legacy challenge type to render HTML as-is"""
+ """challenge type to render HTML as-is"""
body = CharField()
+ component = CharField(default="xak-flow-shell")
class WithUserInfoChallenge(Challenge):
@@ -67,6 +69,7 @@ class AccessDeniedChallenge(Challenge):
"""Challenge when a flow's active stage calls `stage_invalid()`."""
error_message = CharField(required=False)
+ component = CharField(default="ak-stage-access-denied")
class PermissionSerializer(PassiveSerializer):
@@ -80,6 +83,7 @@ class ChallengeResponse(PassiveSerializer):
"""Base class for all challenge responses"""
stage: Optional["StageView"]
+ component = CharField(default="")
def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None)
diff --git a/authentik/flows/views.py b/authentik/flows/views.py
index 509f86fd4..ee6143bb3 100644
--- a/authentik/flows/views.py
+++ b/authentik/flows/views.py
@@ -11,7 +11,12 @@ from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View
from drf_spectacular.types import OpenApiTypes
-from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
+from drf_spectacular.utils import (
+ OpenApiParameter,
+ OpenApiResponse,
+ PolymorphicProxySerializer,
+ extend_schema,
+)
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception
@@ -22,10 +27,12 @@ from authentik.events.models import cleanse_dict
from authentik.flows.challenge import (
AccessDeniedChallenge,
Challenge,
+ ChallengeResponse,
ChallengeTypes,
HttpChallengeResponse,
RedirectChallenge,
ShellChallenge,
+ WithUserInfoChallenge,
)
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
@@ -35,7 +42,7 @@ from authentik.flows.planner import (
FlowPlan,
FlowPlanner,
)
-from authentik.lib.utils.reflection import class_to_path
+from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
LOGGER = get_logger()
@@ -46,6 +53,43 @@ SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
SESSION_KEY_GET = "authentik_flows_get"
+def challenge_types():
+ """This is a workaround for PolymorphicProxySerializer not accepting a callable for
+ `serializers`. This function returns a class which is an iterator, which returns the
+ subclasses of Challenge, and Challenge itself."""
+
+ class Inner(dict):
+ """dummy class with custom callback on .items()"""
+
+ def items(self):
+ mapping = {}
+ classes = all_subclasses(Challenge)
+ classes.remove(WithUserInfoChallenge)
+ for cls in classes:
+ mapping[cls().fields["component"].default] = cls
+ return mapping.items()
+
+ return Inner()
+
+
+def challenge_response_types():
+ """This is a workaround for PolymorphicProxySerializer not accepting a callable for
+ `serializers`. This function returns a class which is an iterator, which returns the
+ subclasses of Challenge, and Challenge itself."""
+
+ class Inner(dict):
+ """dummy class with custom callback on .items()"""
+
+ def items(self):
+ mapping = {}
+ classes = all_subclasses(ChallengeResponse)
+ for cls in classes:
+ mapping[cls(stage=None).fields["component"].default] = cls
+ return mapping.items()
+
+ return Inner()
+
+
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views"""
@@ -126,7 +170,11 @@ class FlowExecutorView(APIView):
@extend_schema(
responses={
- 200: Challenge(),
+ 200: PolymorphicProxySerializer(
+ component_name="Challenge",
+ serializers=challenge_types(),
+ resource_type_field_name="component",
+ ),
404: OpenApiResponse(
description="No Token found"
), # This error can be raised by the email stage
@@ -159,8 +207,18 @@ class FlowExecutorView(APIView):
return to_stage_response(request, FlowErrorResponse(request, exc))
@extend_schema(
- responses={200: Challenge()},
- request=OpenApiTypes.OBJECT,
+ responses={
+ 200: PolymorphicProxySerializer(
+ component_name="Challenge",
+ serializers=challenge_types(),
+ resource_type_field_name="component",
+ ),
+ },
+ request=PolymorphicProxySerializer(
+ component_name="ChallengeResponse",
+ serializers=challenge_response_types(),
+ resource_type_field_name="component",
+ ),
parameters=[
OpenApiParameter(
name="query",
diff --git a/authentik/providers/saml/views/flows.py b/authentik/providers/saml/views/flows.py
index e6ebb368e..094fe2296 100644
--- a/authentik/providers/saml/views/flows.py
+++ b/authentik/providers/saml/views/flows.py
@@ -34,6 +34,7 @@ class AutosubmitChallenge(Challenge):
url = CharField()
attrs = DictField(child=CharField())
+ component = CharField(default="ak-stage-autosubmit")
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
diff --git a/authentik/sources/plex/models.py b/authentik/sources/plex/models.py
index 9953cd290..c83483db3 100644
--- a/authentik/sources/plex/models.py
+++ b/authentik/sources/plex/models.py
@@ -17,6 +17,7 @@ class PlexAuthenticationChallenge(Challenge):
client_id = CharField()
slug = CharField()
+ component = CharField(default="ak-flow-sources-plex")
class PlexSource(Source):
diff --git a/authentik/stages/authenticator_duo/stage.py b/authentik/stages/authenticator_duo/stage.py
index cee9f8814..4bbc1b6fa 100644
--- a/authentik/stages/authenticator_duo/stage.py
+++ b/authentik/stages/authenticator_duo/stage.py
@@ -25,6 +25,7 @@ class AuthenticatorDuoChallenge(WithUserInfoChallenge):
activation_barcode = CharField()
activation_code = CharField()
stage_uuid = CharField()
+ component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoStageView(ChallengeStageView):
@@ -42,7 +43,6 @@ class AuthenticatorDuoStageView(ChallengeStageView):
return AuthenticatorDuoChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-authenticator-duo",
"activation_barcode": enroll["activation_barcode"],
"activation_code": enroll["activation_code"],
"stage_uuid": stage.stage_uuid,
diff --git a/authentik/stages/authenticator_static/stage.py b/authentik/stages/authenticator_static/stage.py
index 6cab085c5..9212cb6c6 100644
--- a/authentik/stages/authenticator_static/stage.py
+++ b/authentik/stages/authenticator_static/stage.py
@@ -22,6 +22,7 @@ class AuthenticatorStaticChallenge(WithUserInfoChallenge):
"""Static authenticator challenge"""
codes = ListField(child=CharField())
+ component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticStageView(ChallengeStageView):
@@ -32,7 +33,6 @@ class AuthenticatorStaticStageView(ChallengeStageView):
return AuthenticatorStaticChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-authenticator-static",
"codes": [token.token for token in tokens],
}
)
diff --git a/authentik/stages/authenticator_totp/stage.py b/authentik/stages/authenticator_totp/stage.py
index 84adbd398..9e5bb8cbb 100644
--- a/authentik/stages/authenticator_totp/stage.py
+++ b/authentik/stages/authenticator_totp/stage.py
@@ -25,6 +25,7 @@ class AuthenticatorTOTPChallenge(WithUserInfoChallenge):
"""TOTP Setup challenge"""
config_url = CharField()
+ component = CharField(default="ak-stage-authenticator-totp")
class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
@@ -33,6 +34,7 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
device: TOTPDevice
code = IntegerField()
+ component = CharField(default="ak-stage-authenticator-totp")
def validate_code(self, code: int) -> int:
"""Validate totp code"""
@@ -52,7 +54,6 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
return AuthenticatorTOTPChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-authenticator-totp",
"config_url": device.config_url,
}
)
diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py
index 807503ce5..15888aa5f 100644
--- a/authentik/stages/authenticator_validate/stage.py
+++ b/authentik/stages/authenticator_validate/stage.py
@@ -30,18 +30,20 @@ LOGGER = get_logger()
PER_DEVICE_CLASSES = [DeviceClasses.WEBAUTHN]
-class AuthenticatorChallenge(WithUserInfoChallenge):
+class AuthenticatorValidationChallenge(WithUserInfoChallenge):
"""Authenticator challenge"""
device_challenges = ListField(child=DeviceChallenge())
+ component = CharField(default="ak-stage-authenticator-validate")
-class AuthenticatorChallengeResponse(ChallengeResponse):
+class AuthenticatorValidationChallengeResponse(ChallengeResponse):
"""Challenge used for Code-based and WebAuthn authenticators"""
code = CharField(required=False)
webauthn = JSONField(required=False)
duo = IntegerField(required=False)
+ component = CharField(default="ak-stage-authenticator-validate")
def _challenge_allowed(self, classes: list):
device_challenges: list[dict] = self.stage.request.session.get(
@@ -83,7 +85,7 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
class AuthenticatorValidateStageView(ChallengeStageView):
"""Authenticator Validation"""
- response_class = AuthenticatorChallengeResponse
+ response_class = AuthenticatorValidationChallengeResponse
def get_device_challenges(self) -> list[dict]:
"""Get a list of all device challenges applicable for the current stage"""
@@ -144,19 +146,18 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
- def get_challenge(self) -> AuthenticatorChallenge:
+ def get_challenge(self) -> AuthenticatorValidationChallenge:
challenges = self.request.session["device_challenges"]
- return AuthenticatorChallenge(
+ return AuthenticatorValidationChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-authenticator-validate",
"device_challenges": challenges,
}
)
# pylint: disable=unused-argument
def challenge_valid(
- self, challenge: AuthenticatorChallengeResponse
+ self, challenge: AuthenticatorValidationChallengeResponse
) -> HttpResponse:
# All validation is done by the serializer
return self.executor.stage_ok()
diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py
index ce4a0aa94..12c5e888d 100644
--- a/authentik/stages/authenticator_webauthn/stage.py
+++ b/authentik/stages/authenticator_webauthn/stage.py
@@ -2,7 +2,7 @@
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
-from rest_framework.fields import JSONField
+from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from webauthn.webauthn import (
@@ -41,12 +41,14 @@ class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
"""WebAuthn Challenge"""
registration = JSONField()
+ component = CharField(default="ak-stage-authenticator-webauthn")
class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
"""WebAuthn Challenge response"""
response = JSONField()
+ component = CharField(default="ak-stage-authenticator-webauthn")
request: HttpRequest
user: User
@@ -134,7 +136,6 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
return AuthenticatorWebAuthnChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-authenticator-webauthn",
"registration": registration_dict,
}
)
diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py
index 98db7728a..1bc0d8492 100644
--- a/authentik/stages/captcha/stage.py
+++ b/authentik/stages/captcha/stage.py
@@ -21,12 +21,14 @@ class CaptchaChallenge(WithUserInfoChallenge):
"""Site public key"""
site_key = CharField()
+ component = CharField(default="ak-stage-captcha")
class CaptchaChallengeResponse(ChallengeResponse):
"""Validate captcha token"""
token = CharField()
+ component = CharField(default="ak-stage-captcha")
def validate_token(self, token: str) -> str:
"""Validate captcha token"""
@@ -64,7 +66,6 @@ class CaptchaStageView(ChallengeStageView):
return CaptchaChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-captcha",
"site_key": self.executor.current_stage.public_key,
}
)
diff --git a/authentik/stages/consent/stage.py b/authentik/stages/consent/stage.py
index aba15031b..9ba75a2a0 100644
--- a/authentik/stages/consent/stage.py
+++ b/authentik/stages/consent/stage.py
@@ -25,11 +25,14 @@ class ConsentChallenge(WithUserInfoChallenge):
header_text = CharField()
permissions = PermissionSerializer(many=True)
+ component = CharField(default="ak-stage-consent")
class ConsentChallengeResponse(ChallengeResponse):
"""Consent challenge response, any valid response request is valid"""
+ component = CharField(default="ak-stage-consent")
+
class ConsentStageView(ChallengeStageView):
"""Simple consent checker."""
@@ -40,7 +43,6 @@ class ConsentStageView(ChallengeStageView):
challenge = ConsentChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-consent",
}
)
if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
diff --git a/authentik/stages/dummy/stage.py b/authentik/stages/dummy/stage.py
index 3ecef6f65..3732c71de 100644
--- a/authentik/stages/dummy/stage.py
+++ b/authentik/stages/dummy/stage.py
@@ -1,5 +1,6 @@
"""authentik multi-stage authentication engine"""
from django.http.response import HttpResponse
+from rest_framework.fields import CharField
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.stage import ChallengeStageView
@@ -8,10 +9,14 @@ from authentik.flows.stage import ChallengeStageView
class DummyChallenge(Challenge):
"""Dummy challenge"""
+ component = CharField(default="ak-stage-dummy")
+
class DummyChallengeResponse(ChallengeResponse):
"""Dummy challenge response"""
+ component = CharField(default="ak-stage-dummy")
+
class DummyStageView(ChallengeStageView):
"""Dummy stage for testing with multiple stages"""
@@ -25,7 +30,6 @@ class DummyStageView(ChallengeStageView):
return DummyChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-dummy",
"title": self.executor.current_stage.name,
}
)
diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py
index 7c4c55831..ae672f1b0 100644
--- a/authentik/stages/email/stage.py
+++ b/authentik/stages/email/stage.py
@@ -8,6 +8,7 @@ from django.urls import reverse
from django.utils.http import urlencode
from django.utils.timezone import now
from django.utils.translation import gettext as _
+from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
@@ -28,11 +29,15 @@ PLAN_CONTEXT_EMAIL_SENT = "email_sent"
class EmailChallenge(Challenge):
"""Email challenge"""
+ component = CharField(default="ak-stage-email")
+
class EmailChallengeResponse(ChallengeResponse):
"""Email challenge resposen. No fields. This challenge is
always declared invalid to give the user a chance to retry"""
+ component = CharField(default="ak-stage-email")
+
def validate(self, data):
raise ValidationError("")
@@ -97,7 +102,6 @@ class EmailStageView(ChallengeStageView):
challenge = EmailChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-email",
"title": "Email sent.",
}
)
diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py
index 625546c0f..69e2053f9 100644
--- a/authentik/stages/identification/stage.py
+++ b/authentik/stages/identification/stage.py
@@ -36,11 +36,15 @@ class IdentificationChallenge(Challenge):
primary_action = CharField()
sources = UILoginButtonSerializer(many=True, required=False)
+ component = CharField(default="ak-stage-identification")
+
class IdentificationChallengeResponse(ChallengeResponse):
"""Identification challenge"""
uid_field = CharField()
+ component = CharField(default="ak-stage-identification")
+
pre_user: Optional[User] = None
def validate_uid_field(self, value: str) -> str:
@@ -81,7 +85,6 @@ class IdentificationStageView(ChallengeStageView):
challenge = IdentificationChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-identification",
"primary_action": _("Log in"),
"user_fields": current_stage.user_fields,
}
diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py
index 73cf9b1db..a548cc7be 100644
--- a/authentik/stages/password/stage.py
+++ b/authentik/stages/password/stage.py
@@ -63,12 +63,16 @@ class PasswordChallenge(WithUserInfoChallenge):
recovery_url = CharField(required=False)
+ component = CharField(default="ak-stage-password")
+
class PasswordChallengeResponse(ChallengeResponse):
"""Password challenge response"""
password = CharField()
+ component = CharField(default="ak-stage-password")
+
class PasswordStageView(ChallengeStageView):
"""Authentication stage which authenticates against django's AuthBackend"""
@@ -79,7 +83,6 @@ class PasswordStageView(ChallengeStageView):
challenge = PasswordChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-password",
}
)
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
diff --git a/authentik/stages/prompt/stage.py b/authentik/stages/prompt/stage.py
index 8b76e614e..a1d4c3038 100644
--- a/authentik/stages/prompt/stage.py
+++ b/authentik/stages/prompt/stage.py
@@ -26,7 +26,7 @@ LOGGER = get_logger()
PLAN_CONTEXT_PROMPT = "prompt_data"
-class PromptSerializer(PassiveSerializer):
+class StagePromptSerializer(PassiveSerializer):
"""Serializer for a single Prompt field"""
field_key = CharField()
@@ -40,17 +40,22 @@ class PromptSerializer(PassiveSerializer):
class PromptChallenge(Challenge):
"""Initial challenge being sent, define fields"""
- fields = PromptSerializer(many=True)
+ fields = StagePromptSerializer(many=True)
+ component = CharField(default="ak-stage-prompt")
class PromptResponseChallenge(ChallengeResponse):
"""Validate response, fields are dynamically created based
on the stage"""
- def __init__(self, *args, stage: PromptStage, plan: FlowPlan, **kwargs):
+ component = CharField(default="ak-stage-prompt")
+
+ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.stage = stage
- self.plan = plan
+ self.stage: PromptStage = kwargs.pop("stage", None)
+ self.plan: FlowPlan = kwargs.pop("plan", None)
+ if not self.stage:
+ return
# list() is called so we only load the fields once
fields = list(self.stage.fields.all())
for field in fields:
@@ -159,8 +164,7 @@ class PromptStageView(ChallengeStageView):
challenge = PromptChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
- "component": "ak-stage-prompt",
- "fields": [PromptSerializer(field).data for field in fields],
+ "fields": [StagePromptSerializer(field).data for field in fields],
},
)
return challenge
diff --git a/schema.yml b/schema.yml
index f27706fc6..159f7a6c5 100644
--- a/schema.yml
+++ b/schema.yml
@@ -3550,16 +3550,13 @@ paths:
content:
application/json:
schema:
- type: object
- additionalProperties: {}
+ $ref: '#/components/schemas/ChallengeResponseRequest'
application/x-www-form-urlencoded:
schema:
- type: object
- additionalProperties: {}
+ $ref: '#/components/schemas/ChallengeResponseRequest'
multipart/form-data:
schema:
- type: object
- additionalProperties: {}
+ $ref: '#/components/schemas/ChallengeResponseRequest'
security:
- authentik: []
- cookieAuth: []
@@ -14924,6 +14921,29 @@ paths:
$ref: '#/components/schemas/GenericError'
components:
schemas:
+ AccessDeniedChallenge:
+ type: object
+ description: Challenge when a flow's active stage calls `stage_invalid()`.
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-access-denied
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ error_message:
+ type: string
+ required:
+ - type
ActionEnum:
enum:
- login
@@ -15138,6 +15158,42 @@ components:
If empty, user will not be able to configure this stage.
required:
- name
+ AuthenticatorDuoChallenge:
+ type: object
+ description: Duo Challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-authenticator-duo
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ activation_barcode:
+ type: string
+ activation_code:
+ type: string
+ stage_uuid:
+ type: string
+ required:
+ - activation_barcode
+ - activation_code
+ - pending_user
+ - pending_user_avatar
+ - stage_uuid
+ - type
AuthenticatorDuoStage:
type: object
description: AuthenticatorDuoStage Serializer
@@ -15208,6 +15264,38 @@ components:
- client_id
- client_secret
- name
+ AuthenticatorStaticChallenge:
+ type: object
+ description: Static authenticator challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-authenticator-static
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ codes:
+ type: array
+ items:
+ type: string
+ required:
+ - codes
+ - pending_user
+ - pending_user_avatar
+ - type
AuthenticatorStaticStage:
type: object
description: AuthenticatorStaticStage Serializer
@@ -15270,6 +15358,47 @@ components:
minimum: -2147483648
required:
- name
+ AuthenticatorTOTPChallenge:
+ type: object
+ description: TOTP Setup challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-authenticator-totp
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ config_url:
+ type: string
+ required:
+ - config_url
+ - pending_user
+ - pending_user_avatar
+ - type
+ AuthenticatorTOTPChallengeResponseRequest:
+ type: object
+ description: TOTP Challenge response, device is set by get_response_instance
+ properties:
+ component:
+ type: string
+ default: ak-stage-authenticator-totp
+ code:
+ type: integer
+ required:
+ - code
AuthenticatorTOTPStage:
type: object
description: AuthenticatorTOTPStage Serializer
@@ -15406,6 +15535,124 @@ components:
is not prompted again.
required:
- name
+ AuthenticatorValidationChallenge:
+ type: object
+ description: Authenticator challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-authenticator-validate
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ device_challenges:
+ type: array
+ items:
+ $ref: '#/components/schemas/DeviceChallenge'
+ required:
+ - device_challenges
+ - pending_user
+ - pending_user_avatar
+ - type
+ AuthenticatorValidationChallengeResponseRequest:
+ type: object
+ description: Challenge used for Code-based and WebAuthn authenticators
+ properties:
+ component:
+ type: string
+ default: ak-stage-authenticator-validate
+ code:
+ type: string
+ webauthn:
+ type: object
+ additionalProperties: {}
+ duo:
+ type: integer
+ AuthenticatorWebAuthnChallenge:
+ type: object
+ description: WebAuthn Challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-authenticator-webauthn
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ registration:
+ type: object
+ additionalProperties: {}
+ required:
+ - pending_user
+ - pending_user_avatar
+ - registration
+ - type
+ AuthenticatorWebAuthnChallengeResponseRequest:
+ type: object
+ description: WebAuthn Challenge response
+ properties:
+ component:
+ type: string
+ default: ak-stage-authenticator-webauthn
+ response:
+ type: object
+ additionalProperties: {}
+ required:
+ - response
+ AutosubmitChallenge:
+ type: object
+ description: Autosubmit challenge used to send and navigate a POST request
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-autosubmit
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ url:
+ type: string
+ attrs:
+ type: object
+ additionalProperties:
+ type: string
+ required:
+ - attrs
+ - type
+ - url
BackendsEnum:
enum:
- django.contrib.auth.backends.ModelBackend
@@ -15430,6 +15677,47 @@ components:
enum:
- can_save_media
type: string
+ CaptchaChallenge:
+ type: object
+ description: Site public key
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-captcha
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ site_key:
+ type: string
+ required:
+ - pending_user
+ - pending_user_avatar
+ - site_key
+ - type
+ CaptchaChallengeResponseRequest:
+ type: object
+ description: Validate captcha token
+ properties:
+ component:
+ type: string
+ default: ak-stage-captcha
+ token:
+ type: string
+ required:
+ - token
CaptchaStage:
type: object
description: CaptchaStage Serializer
@@ -15557,33 +15845,75 @@ components:
- certificate_data
- name
Challenge:
- type: object
- description: |-
- Challenge that gets sent to the client based on which stage
- is currently active
- properties:
- type:
- $ref: '#/components/schemas/ChallengeChoices'
- component:
- type: string
- title:
- type: string
- background:
- type: string
- response_errors:
- type: object
- additionalProperties:
- type: array
- items:
- $ref: '#/components/schemas/ErrorDetail'
- required:
- - type
+ oneOf:
+ - $ref: '#/components/schemas/AccessDeniedChallenge'
+ - $ref: '#/components/schemas/AuthenticatorDuoChallenge'
+ - $ref: '#/components/schemas/AuthenticatorStaticChallenge'
+ - $ref: '#/components/schemas/AuthenticatorTOTPChallenge'
+ - $ref: '#/components/schemas/AuthenticatorValidationChallenge'
+ - $ref: '#/components/schemas/AuthenticatorWebAuthnChallenge'
+ - $ref: '#/components/schemas/AutosubmitChallenge'
+ - $ref: '#/components/schemas/CaptchaChallenge'
+ - $ref: '#/components/schemas/ConsentChallenge'
+ - $ref: '#/components/schemas/DummyChallenge'
+ - $ref: '#/components/schemas/EmailChallenge'
+ - $ref: '#/components/schemas/IdentificationChallenge'
+ - $ref: '#/components/schemas/PasswordChallenge'
+ - $ref: '#/components/schemas/PlexAuthenticationChallenge'
+ - $ref: '#/components/schemas/PromptChallenge'
+ - $ref: '#/components/schemas/RedirectChallenge'
+ - $ref: '#/components/schemas/ShellChallenge'
+ discriminator:
+ propertyName: component
+ mapping:
+ ak-stage-access-denied: '#/components/schemas/AccessDeniedChallenge'
+ ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallenge'
+ ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge'
+ ak-stage-authenticator-totp: '#/components/schemas/AuthenticatorTOTPChallenge'
+ ak-stage-authenticator-validate: '#/components/schemas/AuthenticatorValidationChallenge'
+ ak-stage-authenticator-webauthn: '#/components/schemas/AuthenticatorWebAuthnChallenge'
+ ak-stage-autosubmit: '#/components/schemas/AutosubmitChallenge'
+ ak-stage-captcha: '#/components/schemas/CaptchaChallenge'
+ ak-stage-consent: '#/components/schemas/ConsentChallenge'
+ ak-stage-dummy: '#/components/schemas/DummyChallenge'
+ ak-stage-email: '#/components/schemas/EmailChallenge'
+ ak-stage-identification: '#/components/schemas/IdentificationChallenge'
+ ak-stage-password: '#/components/schemas/PasswordChallenge'
+ ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge'
+ ak-stage-prompt: '#/components/schemas/PromptChallenge'
+ xak-flow-redirect: '#/components/schemas/RedirectChallenge'
+ xak-flow-shell: '#/components/schemas/ShellChallenge'
ChallengeChoices:
enum:
- native
- shell
- redirect
type: string
+ ChallengeResponseRequest:
+ oneOf:
+ - $ref: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest'
+ - $ref: '#/components/schemas/AuthenticatorValidationChallengeResponseRequest'
+ - $ref: '#/components/schemas/AuthenticatorWebAuthnChallengeResponseRequest'
+ - $ref: '#/components/schemas/CaptchaChallengeResponseRequest'
+ - $ref: '#/components/schemas/ConsentChallengeResponseRequest'
+ - $ref: '#/components/schemas/DummyChallengeResponseRequest'
+ - $ref: '#/components/schemas/EmailChallengeResponseRequest'
+ - $ref: '#/components/schemas/IdentificationChallengeResponseRequest'
+ - $ref: '#/components/schemas/PasswordChallengeResponseRequest'
+ - $ref: '#/components/schemas/PromptResponseChallengeRequest'
+ discriminator:
+ propertyName: component
+ mapping:
+ ak-stage-authenticator-totp: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest'
+ ak-stage-authenticator-validate: '#/components/schemas/AuthenticatorValidationChallengeResponseRequest'
+ ak-stage-authenticator-webauthn: '#/components/schemas/AuthenticatorWebAuthnChallengeResponseRequest'
+ ak-stage-captcha: '#/components/schemas/CaptchaChallengeResponseRequest'
+ ak-stage-consent: '#/components/schemas/ConsentChallengeResponseRequest'
+ ak-stage-dummy: '#/components/schemas/DummyChallengeResponseRequest'
+ ak-stage-email: '#/components/schemas/EmailChallengeResponseRequest'
+ ak-stage-identification: '#/components/schemas/IdentificationChallengeResponseRequest'
+ ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
+ ak-stage-prompt: '#/components/schemas/PromptResponseChallengeRequest'
ClientTypeEnum:
enum:
- confidential
@@ -15625,6 +15955,48 @@ components:
- error_reporting_environment
- error_reporting_send_pii
- ui_footer_links
+ ConsentChallenge:
+ type: object
+ description: Challenge info for consent screens
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-consent
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ header_text:
+ type: string
+ permissions:
+ type: array
+ items:
+ $ref: '#/components/schemas/Permission'
+ required:
+ - header_text
+ - pending_user
+ - pending_user_avatar
+ - permissions
+ - type
+ ConsentChallengeResponseRequest:
+ type: object
+ description: Consent challenge response, any valid response request is valid
+ properties:
+ component:
+ type: string
+ default: ak-stage-consent
ConsentStage:
type: object
description: ConsentStage Serializer
@@ -15740,6 +16112,21 @@ components:
$ref: '#/components/schemas/FlowRequest'
required:
- name
+ DeviceChallenge:
+ type: object
+ description: Single device challenge
+ properties:
+ device_class:
+ type: string
+ device_uid:
+ type: string
+ challenge:
+ type: object
+ additionalProperties: {}
+ required:
+ - challenge
+ - device_class
+ - device_uid
DeviceClassesEnum:
enum:
- static
@@ -15837,6 +16224,34 @@ components:
required:
- name
- url
+ DummyChallenge:
+ type: object
+ description: Dummy challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-dummy
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ required:
+ - type
+ DummyChallengeResponseRequest:
+ type: object
+ description: Dummy challenge response
+ properties:
+ component:
+ type: string
+ default: ak-stage-dummy
DummyPolicy:
type: object
description: Dummy Policy Serializer
@@ -15944,6 +16359,36 @@ components:
$ref: '#/components/schemas/FlowRequest'
required:
- name
+ EmailChallenge:
+ type: object
+ description: Email challenge
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-email
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ required:
+ - type
+ EmailChallengeResponseRequest:
+ type: object
+ description: |-
+ Email challenge resposen. No fields. This challenge is
+ always declared invalid to give the user a chance to retry
+ properties:
+ component:
+ type: string
+ default: ak-stage-email
EmailStage:
type: object
description: EmailStage Serializer
@@ -16640,6 +17085,57 @@ components:
minimum: -2147483648
required:
- ip
+ IdentificationChallenge:
+ type: object
+ description: Identification challenges with all UI elements
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-identification
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ user_fields:
+ type: array
+ items:
+ type: string
+ nullable: true
+ application_pre:
+ type: string
+ enroll_url:
+ type: string
+ recovery_url:
+ type: string
+ primary_action:
+ type: string
+ sources:
+ type: array
+ items:
+ $ref: '#/components/schemas/UILoginButton'
+ required:
+ - primary_action
+ - type
+ - user_fields
+ IdentificationChallengeResponseRequest:
+ type: object
+ description: Identification challenge
+ properties:
+ component:
+ type: string
+ default: ak-stage-identification
+ uid_field:
+ type: string
+ required:
+ - uid_field
IdentificationStage:
type: object
description: IdentificationStage Serializer
@@ -20375,6 +20871,46 @@ components:
required:
- pagination
- results
+ PasswordChallenge:
+ type: object
+ description: Password challenge UI fields
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-password
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ pending_user:
+ type: string
+ pending_user_avatar:
+ type: string
+ recovery_url:
+ type: string
+ required:
+ - pending_user
+ - pending_user_avatar
+ - type
+ PasswordChallengeResponseRequest:
+ type: object
+ description: Password challenge response
+ properties:
+ component:
+ type: string
+ default: ak-stage-password
+ password:
+ type: string
+ required:
+ - password
PasswordExpiryPolicy:
type: object
description: Password Expiry Policy Serializer
@@ -22038,6 +22574,44 @@ components:
name:
type: string
maxLength: 200
+ Permission:
+ type: object
+ description: Permission used for consent
+ properties:
+ name:
+ type: string
+ id:
+ type: string
+ required:
+ - id
+ - name
+ PlexAuthenticationChallenge:
+ type: object
+ description: Challenge shown to the user in identification stage
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-flow-sources-plex
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ client_id:
+ type: string
+ slug:
+ type: string
+ required:
+ - client_id
+ - slug
+ - type
PlexSource:
type: object
description: Plex Source Serializer
@@ -22359,6 +22933,32 @@ components:
- label
- pk
- type
+ PromptChallenge:
+ type: object
+ description: Initial challenge being sent, define fields
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: ak-stage-prompt
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ fields:
+ type: array
+ items:
+ $ref: '#/components/schemas/StagePrompt'
+ required:
+ - fields
+ - type
PromptRequest:
type: object
description: Prompt Serializer
@@ -22388,6 +22988,15 @@ components:
- field_key
- label
- type
+ PromptResponseChallengeRequest:
+ type: object
+ description: |-
+ Validate response, fields are dynamically created based
+ on the stage
+ properties:
+ component:
+ type: string
+ default: ak-stage-prompt
PromptStage:
type: object
description: PromptStage Serializer
@@ -22789,12 +23398,13 @@ components:
properties:
type:
$ref: '#/components/schemas/ChallengeChoices'
- component:
- type: string
title:
type: string
background:
type: string
+ component:
+ type: string
+ default: xak-flow-redirect
response_errors:
type: object
additionalProperties:
@@ -23462,6 +24072,30 @@ components:
- warning
- alert
type: string
+ ShellChallenge:
+ type: object
+ description: challenge type to render HTML as-is
+ properties:
+ type:
+ $ref: '#/components/schemas/ChallengeChoices'
+ title:
+ type: string
+ background:
+ type: string
+ component:
+ type: string
+ default: xak-flow-shell
+ response_errors:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/ErrorDetail'
+ body:
+ type: string
+ required:
+ - body
+ - type
SignatureAlgorithmEnum:
enum:
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
@@ -23591,6 +24225,29 @@ components:
- pk
- verbose_name
- verbose_name_plural
+ StagePrompt:
+ type: object
+ description: Serializer for a single Prompt field
+ properties:
+ field_key:
+ type: string
+ label:
+ type: string
+ type:
+ type: string
+ required:
+ type: boolean
+ placeholder:
+ type: string
+ order:
+ type: integer
+ required:
+ - field_key
+ - label
+ - order
+ - placeholder
+ - required
+ - type
StageRequest:
type: object
description: Stage Serializer
@@ -23818,6 +24475,21 @@ components:
- description
- model_name
- name
+ UILoginButton:
+ type: object
+ description: Serializer for Login buttons of sources
+ properties:
+ name:
+ type: string
+ challenge:
+ type: object
+ additionalProperties: {}
+ icon_url:
+ type: string
+ nullable: true
+ required:
+ - challenge
+ - name
User:
type: object
description: User Serializer
diff --git a/web/src/api/Flows.ts b/web/src/api/Flows.ts
index 367ded8e1..2b147cc87 100644
--- a/web/src/api/Flows.ts
+++ b/web/src/api/Flows.ts
@@ -8,23 +8,3 @@ export interface Error {
export interface ErrorDict {
[key: string]: Error[];
}
-
-export interface Challenge {
- type: ChallengeChoices;
- component?: string;
- title?: string;
- response_errors?: ErrorDict;
-}
-
-export interface WithUserInfoChallenge extends Challenge {
- pending_user: string;
- pending_user_avatar: string;
-}
-
-export interface ShellChallenge extends Challenge {
- body: string;
-}
-
-export interface RedirectChallenge extends Challenge {
- to: string;
-}
diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts
index 0cb048c6e..6abe6a430 100644
--- a/web/src/flows/FlowExecutor.ts
+++ b/web/src/flows/FlowExecutor.ts
@@ -25,29 +25,16 @@ import "./stages/identification/IdentificationStage";
import "./stages/password/PasswordStage";
import "./stages/prompt/PromptStage";
import "./sources/plex/PlexLoginInit";
-import { ShellChallenge, RedirectChallenge } from "../api/Flows";
-import { IdentificationChallenge } from "./stages/identification/IdentificationStage";
-import { PasswordChallenge } from "./stages/password/PasswordStage";
-import { ConsentChallenge } from "./stages/consent/ConsentStage";
-import { EmailChallenge } from "./stages/email/EmailStage";
-import { AutosubmitChallenge } from "./stages/autosubmit/AutosubmitStage";
-import { PromptChallenge } from "./stages/prompt/PromptStage";
-import { AuthenticatorTOTPChallenge } from "./stages/authenticator_totp/AuthenticatorTOTPStage";
-import { AuthenticatorStaticChallenge } from "./stages/authenticator_static/AuthenticatorStaticStage";
-import { AuthenticatorValidateStageChallenge } from "./stages/authenticator_validate/AuthenticatorValidateStage";
-import { WebAuthnAuthenticatorRegisterChallenge } from "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
-import { CaptchaChallenge } from "./stages/captcha/CaptchaStage";
import { StageHost } from "./stages/base";
-import { Challenge, ChallengeChoices, Config, FlowsApi } from "authentik-api";
+import { Challenge, ChallengeChoices, Config, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
import { config, DEFAULT_CONFIG } from "../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
-import { AccessDeniedChallenge } from "./access_denied/FlowAccessDenied";
import { PFSize } from "../elements/Spinner";
import { TITLE_DEFAULT } from "../constants";
import { configureSentry } from "../api/Sentry";
-import { PlexAuthenticationChallenge } from "./sources/plex/PlexLoginInit";
-import { AuthenticatorDuoChallenge } from "./stages/authenticator_duo/AuthenticatorDuoStage";
+import { ChallengeResponseRequest } from "authentik-api/dist/models/ChallengeResponseRequest";
+
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost {
@@ -112,18 +99,18 @@ export class FlowExecutor extends LitElement implements StageHost {
});
}
- submit
${this.challenge.error_message}
`} +${this.challenge.errorMessage}
`} diff --git a/web/src/flows/sources/plex/PlexLoginInit.ts b/web/src/flows/sources/plex/PlexLoginInit.ts index f78c7329d..33d12282c 100644 --- a/web/src/flows/sources/plex/PlexLoginInit.ts +++ b/web/src/flows/sources/plex/PlexLoginInit.ts @@ -1,5 +1,5 @@ import { t } from "@lingui/macro"; -import { Challenge } from "authentik-api"; +import { PlexAuthenticationChallenge } from "authentik-api"; import PFLogin from "@patternfly/patternfly/components/Login/login.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; @@ -16,12 +16,6 @@ import { SourcesApi } from "authentik-api"; import { showMessage } from "../../../elements/messages/MessageContainer"; import { MessageLevel } from "../../../elements/messages/Message"; -export interface PlexAuthenticationChallenge extends Challenge { - - client_id: string; - slug: string; - -} @customElement("ak-flow-sources-plex") export class PlexLoginInit extends BaseStage { @@ -34,9 +28,9 @@ export class PlexLoginInit extends BaseStage { } async firstUpdated(): Promise