stages/authenticator_duo: revamp duo enroll status API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#3288
This commit is contained in:
Jens Langhammer 2022-08-08 20:38:06 +02:00
parent 2858682866
commit 54c16129ea
4 changed files with 61 additions and 25 deletions

View File

@ -1,10 +1,16 @@
"""AuthenticatorDuoStage API Views""" """AuthenticatorDuoStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend from django_filters.rest_framework.backends import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes 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 guardian.shortcuts import get_objects_for_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ChoiceField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
@ -57,8 +63,18 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
@extend_schema( @extend_schema(
request=OpenApiTypes.NONE, request=OpenApiTypes.NONE,
responses={ responses={
204: OpenApiResponse(description="Enrollment successful"), 200: inline_serializer(
420: OpenApiResponse(description="Enrollment pending/failed"), "DuoDeviceEnrollmentStatusSerializer",
{
"duo_response": ChoiceField(
(
("success", "Success"),
("waiting", "Waiting"),
("invalid", "Invalid"),
)
)
},
),
}, },
) )
@action(methods=["POST"], detail=True, permission_classes=[]) @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) user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE) activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
if not user_id or not 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) status = client.enroll_status(user_id, activation_code)
if status == "success": return Response({"duo_response": status})
return Response(status=204)
return Response(status=420)
@permission_required( @permission_required(
"", ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"] "", ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"]

View File

@ -94,5 +94,5 @@ class AuthenticatorDuoStageView(ChallengeStageView):
return self.executor.stage_ok() return self.executor.stage_ok()
def cleanup(self): def cleanup(self):
self.request.session.pop(SESSION_KEY_DUO_USER_ID) self.request.session.pop(SESSION_KEY_DUO_USER_ID, None)
self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE) self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE, None)

View File

@ -15076,10 +15076,12 @@ paths:
security: security:
- authentik: [] - authentik: []
responses: responses:
'204': '200':
description: Enrollment successful content:
'420': application/json:
description: Enrollment pending/failed schema:
$ref: '#/components/schemas/DuoDeviceEnrollmentStatus'
description: ''
'400': '400':
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
@ -21877,6 +21879,13 @@ components:
required: required:
- name - name
- pk - pk
DuoDeviceEnrollmentStatus:
type: object
properties:
duo_response:
$ref: '#/components/schemas/DuoResponseEnum'
required:
- duo_response
DuoDeviceRequest: DuoDeviceRequest:
type: object type: object
description: Serializer for Duo authenticator devices description: Serializer for Duo authenticator devices
@ -21888,6 +21897,12 @@ components:
maxLength: 64 maxLength: 64
required: required:
- name - name
DuoResponseEnum:
enum:
- success
- waiting
- invalid
type: string
EmailChallenge: EmailChallenge:
type: object type: object
description: Email challenge description: Email challenge

View File

@ -21,6 +21,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
AuthenticatorDuoChallenge, AuthenticatorDuoChallenge,
AuthenticatorDuoChallengeResponseRequest, AuthenticatorDuoChallengeResponseRequest,
DuoResponseEnum,
StagesApi, StagesApi,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -35,23 +36,29 @@ export class AuthenticatorDuoStage extends BaseStage<
firstUpdated(): void { firstUpdated(): void {
const i = setInterval(() => { const i = setInterval(() => {
this.checkEnrollStatus().then(() => { this.checkEnrollStatus().then((shouldStop) => {
clearInterval(i); if (shouldStop) {
clearInterval(i);
}
}); });
}, 3000); }, 3000);
} }
checkEnrollStatus(): Promise<void> { async checkEnrollStatus(): Promise<boolean> {
return new StagesApi(DEFAULT_CONFIG) const status = await new StagesApi(
.stagesAuthenticatorDuoEnrollmentStatusCreate({ DEFAULT_CONFIG,
stageUuid: this.challenge?.stageUuid || "", ).stagesAuthenticatorDuoEnrollmentStatusCreate({
}) stageUuid: this.challenge?.stageUuid || "",
.then(() => { });
console.debug(`authentik/flows/duo: Enrollment status: ${status.duoResponse}`);
switch (status.duoResponse) {
case DuoResponseEnum.Success:
this.host?.submit({}); this.host?.submit({});
}) return true;
.catch(() => { case DuoResponseEnum.Waiting:
console.debug("authentik/flows/duo: Waiting for auth status"); break;
}); }
return false;
} }
render(): TemplateResult { render(): TemplateResult {