diff --git a/authentik/stages/authenticator_duo/api.py b/authentik/stages/authenticator_duo/api.py index 3c403659f..aed4e0c0a 100644 --- a/authentik/stages/authenticator_duo/api.py +++ b/authentik/stages/authenticator_duo/api.py @@ -1,10 +1,16 @@ """AuthenticatorDuoStage API Views""" from django_filters.rest_framework.backends import DjangoFilterBackend from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from drf_spectacular.utils import ( + OpenApiParameter, + OpenApiResponse, + extend_schema, + inline_serializer, +) from guardian.shortcuts import get_objects_for_user from rest_framework import mixins from rest_framework.decorators import action +from rest_framework.fields import ChoiceField from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser from rest_framework.request import Request @@ -57,8 +63,18 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet): @extend_schema( request=OpenApiTypes.NONE, responses={ - 204: OpenApiResponse(description="Enrollment successful"), - 420: OpenApiResponse(description="Enrollment pending/failed"), + 200: inline_serializer( + "DuoDeviceEnrollmentStatusSerializer", + { + "duo_response": ChoiceField( + ( + ("success", "Success"), + ("waiting", "Waiting"), + ("invalid", "Invalid"), + ) + ) + }, + ), }, ) @action(methods=["POST"], detail=True, permission_classes=[]) @@ -70,11 +86,9 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet): user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID) activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE) if not user_id or not activation_code: - return Response(status=420) + return Response(status=400) status = client.enroll_status(user_id, activation_code) - if status == "success": - return Response(status=204) - return Response(status=420) + return Response({"duo_response": status}) @permission_required( "", ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"] diff --git a/authentik/stages/authenticator_duo/stage.py b/authentik/stages/authenticator_duo/stage.py index 5ea827104..7f1b40225 100644 --- a/authentik/stages/authenticator_duo/stage.py +++ b/authentik/stages/authenticator_duo/stage.py @@ -94,5 +94,5 @@ class AuthenticatorDuoStageView(ChallengeStageView): return self.executor.stage_ok() def cleanup(self): - self.request.session.pop(SESSION_KEY_DUO_USER_ID) - self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE) + self.request.session.pop(SESSION_KEY_DUO_USER_ID, None) + self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE, None) diff --git a/schema.yml b/schema.yml index bd5268986..baf0bbea1 100644 --- a/schema.yml +++ b/schema.yml @@ -15076,10 +15076,12 @@ paths: security: - authentik: [] responses: - '204': - description: Enrollment successful - '420': - description: Enrollment pending/failed + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DuoDeviceEnrollmentStatus' + description: '' '400': $ref: '#/components/schemas/ValidationError' '403': @@ -21877,6 +21879,13 @@ components: required: - name - pk + DuoDeviceEnrollmentStatus: + type: object + properties: + duo_response: + $ref: '#/components/schemas/DuoResponseEnum' + required: + - duo_response DuoDeviceRequest: type: object description: Serializer for Duo authenticator devices @@ -21888,6 +21897,12 @@ components: maxLength: 64 required: - name + DuoResponseEnum: + enum: + - success + - waiting + - invalid + type: string EmailChallenge: type: object description: Email challenge diff --git a/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts b/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts index 5bd275253..f6e73367c 100644 --- a/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts +++ b/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts @@ -21,6 +21,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest, + DuoResponseEnum, StagesApi, } from "@goauthentik/api"; @@ -35,23 +36,29 @@ export class AuthenticatorDuoStage extends BaseStage< firstUpdated(): void { const i = setInterval(() => { - this.checkEnrollStatus().then(() => { - clearInterval(i); + this.checkEnrollStatus().then((shouldStop) => { + if (shouldStop) { + clearInterval(i); + } }); }, 3000); } - checkEnrollStatus(): Promise { - return new StagesApi(DEFAULT_CONFIG) - .stagesAuthenticatorDuoEnrollmentStatusCreate({ - stageUuid: this.challenge?.stageUuid || "", - }) - .then(() => { + async checkEnrollStatus(): Promise { + const status = await new StagesApi( + DEFAULT_CONFIG, + ).stagesAuthenticatorDuoEnrollmentStatusCreate({ + stageUuid: this.challenge?.stageUuid || "", + }); + console.debug(`authentik/flows/duo: Enrollment status: ${status.duoResponse}`); + switch (status.duoResponse) { + case DuoResponseEnum.Success: this.host?.submit({}); - }) - .catch(() => { - console.debug("authentik/flows/duo: Waiting for auth status"); - }); + return true; + case DuoResponseEnum.Waiting: + break; + } + return false; } render(): TemplateResult {