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(formData?: T): Promise { + submit(payload: ChallengeResponseRequest): Promise { + payload.component = this.challenge.component; this.loading = true; - return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolveRaw({ + return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ flowSlug: this.flowSlug, - requestBody: formData || {}, query: window.location.search.substring(1), - }).then((challengeRaw) => { - return challengeRaw.raw.json(); + challengeResponseRequest: payload, }).then((data) => { this.challenge = data; this.postUpdate(); }).catch((e: Response) => { + console.debug(e); this.errorMessage(e.statusText); }).finally(() => { this.loading = false; @@ -135,19 +122,18 @@ export class FlowExecutor extends LitElement implements StageHost { this.config = config; }); this.loading = true; - new FlowsApi(DEFAULT_CONFIG).flowsExecutorGetRaw({ + new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({ flowSlug: this.flowSlug, query: window.location.search.substring(1), - }).then((challengeRaw) => { - return challengeRaw.raw.json(); }).then((challenge) => { - this.challenge = challenge as Challenge; + this.challenge = challenge; // Only set background on first update, flow won't change throughout execution if (this.challenge?.background) { this.setBackground(this.challenge.background); } this.postUpdate(); }).catch((e: Response) => { + console.debug(e); // Catch JSON or Update errors this.errorMessage(e.statusText); }).finally(() => { @@ -202,35 +188,35 @@ export class FlowExecutor extends LitElement implements StageHost { case ChallengeChoices.Native: switch (this.challenge.component) { case "ak-stage-access-denied": - return html``; + return html``; case "ak-stage-identification": - return html``; + return html``; case "ak-stage-password": - return html``; + return html``; case "ak-stage-captcha": - return html``; + return html``; case "ak-stage-consent": - return html``; + return html``; case "ak-stage-dummy": - return html``; + return html``; case "ak-stage-email": - return html``; + return html``; case "ak-stage-autosubmit": - return html``; + return html``; case "ak-stage-prompt": - return html``; + return html``; case "ak-stage-authenticator-totp": - return html``; + return html``; case "ak-stage-authenticator-duo": - return html``; + return html``; case "ak-stage-authenticator-static": - return html``; + return html``; case "ak-stage-authenticator-webauthn": - return html``; + return html``; case "ak-stage-authenticator-validate": - return html``; + return html``; case "ak-flow-sources-plex": - return html``; + return html``; default: break; } @@ -288,8 +274,7 @@ export class FlowExecutor extends LitElement implements StageHost { `; }))} ${this.config?.brandingTitle != "authentik" ? html` -
  • ${t`Powered by authentik`}
  • - ` : html``} +
  • ${t`Powered by authentik`}
  • ` : html``} diff --git a/web/src/flows/access_denied/FlowAccessDenied.ts b/web/src/flows/access_denied/FlowAccessDenied.ts index 0005f23f7..9e0b26e79 100644 --- a/web/src/flows/access_denied/FlowAccessDenied.ts +++ b/web/src/flows/access_denied/FlowAccessDenied.ts @@ -1,4 +1,4 @@ -import { Challenge } from "authentik-api"; +import { AccessDeniedChallenge } from "authentik-api"; import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; import { BaseStage } from "../stages/base"; import PFLogin from "@patternfly/patternfly/components/Login/login.css"; @@ -12,10 +12,6 @@ import { t } from "@lingui/macro"; import "../../elements/EmptyState"; -export interface AccessDeniedChallenge extends Challenge { - error_message?: string; -} - @customElement("ak-stage-access-denied") export class FlowAccessDenied extends BaseStage { @@ -45,9 +41,9 @@ export class FlowAccessDenied extends BaseStage { ${t`Request has been denied.`}

    - ${this.challenge?.error_message && + ${this.challenge?.errorMessage && html`
    -

    ${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 { - const authInfo = await PlexAPIClient.getPin(this.challenge?.client_id || ""); + const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || ""); const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700); - PlexAPIClient.pinPoll(this.challenge?.client_id || "", authInfo.pin.id).then(token => { + PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then(token => { authWindow?.close(); new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({ plexTokenRedeemRequest: { diff --git a/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts b/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts index ce249d95c..957ae3ffd 100644 --- a/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts +++ b/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts @@ -1,6 +1,5 @@ import { t } from "@lingui/macro"; import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; -import { WithUserInfoChallenge } from "../../../api/Flows"; 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"; @@ -13,15 +12,9 @@ import "../../../elements/forms/FormElement"; import "../../../elements/EmptyState"; import "../../FormStatic"; import { FlowURLManager } from "../../../api/legacy"; -import { StagesApi } from "authentik-api"; +import { AuthenticatorDuoChallenge, StagesApi } from "authentik-api"; import { DEFAULT_CONFIG } from "../../../api/Config"; -export interface AuthenticatorDuoChallenge extends WithUserInfoChallenge { - activation_barcode: string; - activation_code: string; - stage_uuid: string; -} - @customElement("ak-stage-authenticator-duo") export class AuthenticatorDuoStage extends BaseStage { @@ -42,10 +35,11 @@ export class AuthenticatorDuoStage extends BaseStage { checkEnrollStatus(): Promise { return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({ - stageUuid: this.challenge?.stage_uuid || "", - }).then(r => { + stageUuid: this.challenge?.stageUuid || "", + }).then(() => { this.host?.submit({}); - }).catch(e => { + }).catch(() => { + console.debug("authentik/flows/duo: Waiting for auth status"); }); } @@ -65,17 +59,17 @@ export class AuthenticatorDuoStage extends BaseStage {
    { this.submitForm(e); }}> + userAvatar="${this.challenge.pendingUserAvatar}" + user=${this.challenge.pendingUser}> - +

    ${t`Alternatively, if your current device has Duo installed, click on this link:`}

    - ${t`Duo activation`} + ${t`Duo activation`}
    `: html`
    - ${this.challenge?.response_errors ? - html`

    ${this.challenge.response_errors["response"][0].string}

    `: + ${this.challenge?.responseErrors ? + html`

    ${this.challenge.responseErrors["response"][0].string}

    `: html``}

    ${this.registerMessage}

    `; } @@ -212,9 +194,9 @@ export class IdentificationStage extends BaseStage {