diff --git a/authentik/core/types.py b/authentik/core/types.py index 3226db60a..a10be69bb 100644 --- a/authentik/core/types.py +++ b/authentik/core/types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import Optional -from rest_framework.fields import CharField, DictField +from rest_framework.fields import CharField from authentik.core.api.utils import PassiveSerializer from authentik.flows.challenge import Challenge @@ -22,14 +22,6 @@ class UILoginButton: icon_url: Optional[str] = None -class UILoginButtonSerializer(PassiveSerializer): - """Serializer for Login buttons of sources""" - - name = CharField() - challenge = DictField() - icon_url = CharField(required=False, allow_null=True) - - class UserSettingSerializer(PassiveSerializer): """Serializer for User settings for stages and sources""" diff --git a/authentik/flows/views.py b/authentik/flows/views.py index 826211ff7..a8b0d9720 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -174,7 +174,7 @@ class FlowExecutorView(APIView): @extend_schema( responses={ 200: PolymorphicProxySerializer( - component_name="FlowChallengeRequest", + component_name="ChallengeTypes", serializers=challenge_types(), resource_type_field_name="component", ), @@ -214,7 +214,7 @@ class FlowExecutorView(APIView): @extend_schema( responses={ 200: PolymorphicProxySerializer( - component_name="FlowChallengeRequest", + component_name="ChallengeTypes", serializers=challenge_types(), resource_type_field_name="component", ), diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index 99297445b..dae45e0a0 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -8,19 +8,25 @@ from django.db.models import Q from django.http import HttpResponse from django.urls import reverse from django.utils.translation import gettext as _ -from rest_framework.fields import BooleanField, CharField, ListField +from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema_field +from rest_framework.fields import ( + BooleanField, + CharField, + DictField, + ListField, +) from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger +from authentik.core.api.utils import PassiveSerializer from authentik.core.models import Application, Source, User -from authentik.core.types import UILoginButtonSerializer from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import ( PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView, ) -from authentik.flows.views import SESSION_KEY_APPLICATION_PRE +from authentik.flows.views import SESSION_KEY_APPLICATION_PRE, challenge_types from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.signals import identification_failed from authentik.stages.password.stage import authenticate @@ -28,6 +34,26 @@ from authentik.stages.password.stage import authenticate LOGGER = get_logger() +@extend_schema_field( + PolymorphicProxySerializer( + component_name="ChallengeTypes", + serializers=challenge_types(), + resource_type_field_name="component", + ) +) +class ChallengeDictWrapper(DictField): + """Wrapper around DictField that annotates itself as challenge proxy""" + + +class LoginSourceSerializer(PassiveSerializer): + """Serializer for Login buttons of sources""" + + name = CharField() + icon_url = CharField(required=False, allow_null=True) + + challenge = ChallengeDictWrapper() + + class IdentificationChallenge(Challenge): """Identification challenges with all UI elements""" @@ -38,7 +64,7 @@ class IdentificationChallenge(Challenge): enroll_url = CharField(required=False) recovery_url = CharField(required=False) primary_action = CharField() - sources = UILoginButtonSerializer(many=True, required=False) + sources = LoginSourceSerializer(many=True, required=False) component = CharField(default="ak-stage-identification") @@ -154,6 +180,7 @@ class IdentificationStageView(ChallengeStageView): button = asdict(ui_login_button) button["challenge"] = ui_login_button.challenge.data ui_sources.append(button) + print(ui_sources) challenge.initial_data["sources"] = ui_sources return challenge diff --git a/schema.yml b/schema.yml index e5698c323..6c56f6177 100644 --- a/schema.yml +++ b/schema.yml @@ -4608,7 +4608,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/FlowChallengeRequest' + $ref: '#/components/schemas/ChallengeTypes' description: '' '404': description: No Token found @@ -4654,7 +4654,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/FlowChallengeRequest' + $ref: '#/components/schemas/ChallengeTypes' description: '' '400': $ref: '#/components/schemas/ValidationError' @@ -18412,12 +18412,75 @@ components: required: - certificate_data - name + ChallT: + type: object + description: |- + Challenge that gets sent to the client based on which stage + is currently active + properties: + type: + $ref: '#/components/schemas/ChallengeChoices' + flow_info: + $ref: '#/components/schemas/ContextualFlowInfo' + component: + type: string + default: '' + response_errors: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/ErrorDetail' + required: + - type ChallengeChoices: enum: - native - shell - redirect type: string + ChallengeTypes: + 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/ChallT' + - $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' + ? '' + : '#/components/schemas/ChallT' + 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' ClientTypeEnum: enum: - confidential @@ -19388,45 +19451,6 @@ components: - slug - stages - title - FlowChallengeRequest: - 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' FlowChallengeResponseRequest: oneOf: - $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest' @@ -19776,7 +19800,7 @@ components: sources: type: array items: - $ref: '#/components/schemas/UILoginButton' + $ref: '#/components/schemas/LoginSource' required: - password_fields - primary_action @@ -20472,6 +20496,20 @@ components: required: - logins_failed_per_1h - logins_per_1h + LoginSource: + type: object + description: Serializer for Login buttons of sources + properties: + name: + type: string + icon_url: + type: string + nullable: true + challenge: + $ref: '#/components/schemas/ChallengeTypes' + required: + - challenge + - name NameIdPolicyEnum: enum: - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress @@ -27526,21 +27564,6 @@ 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 UsedBy: type: object description: A list of all objects referencing the queried object diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index 3292b8838..3a7b6e4ad 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -26,7 +26,7 @@ import "./stages/password/PasswordStage"; import "./stages/prompt/PromptStage"; import "./sources/plex/PlexLoginInit"; import { StageHost } from "./stages/base"; -import { ChallengeChoices, CurrentTenant, FlowChallengeRequest, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api"; +import { ChallengeChoices, CurrentTenant, ChallengeTypes, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api"; import { DEFAULT_CONFIG, tenant } from "../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; import { until } from "lit-html/directives/until"; @@ -40,7 +40,7 @@ export class FlowExecutor extends LitElement implements StageHost { flowSlug: string; @property({attribute: false}) - challenge?: FlowChallengeRequest; + challenge?: ChallengeTypes; @property({type: Boolean}) loading = false; @@ -163,7 +163,7 @@ export class FlowExecutor extends LitElement implements StageHost { ` - } as FlowChallengeRequest; + } as ChallengeTypes; } renderLoading(): TemplateResult { diff --git a/web/src/flows/stages/identification/IdentificationStage.ts b/web/src/flows/stages/identification/IdentificationStage.ts index 82ab644c4..7fee392d1 100644 --- a/web/src/flows/stages/identification/IdentificationStage.ts +++ b/web/src/flows/stages/identification/IdentificationStage.ts @@ -11,7 +11,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import AKGlobal from "../../../authentik.css"; import "../../../elements/forms/FormElement"; import "../../../elements/EmptyState"; -import { FlowChallengeRequest, IdentificationChallenge, IdentificationChallengeResponseRequest, UILoginButton, UserFieldsEnum } from "authentik-api"; +import { IdentificationChallenge, IdentificationChallengeResponseRequest, LoginSource, UserFieldsEnum } from "authentik-api"; export const PasswordManagerPrefill: { password: string | undefined; @@ -110,7 +110,7 @@ export class IdentificationStage extends BaseStage`; if (source.iconUrl) { icon = html`${source.name}`; @@ -118,7 +118,7 @@ export class IdentificationStage extends BaseStage