stages/authenticator_duo: improve setup

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-05-23 21:44:52 +02:00
parent 9f5a3c396d
commit 65522186f1
6 changed files with 37 additions and 51 deletions

View File

@ -47,7 +47,7 @@ class AuthenticatorDuoStageViewSet(ModelViewSet):
request=OpenApiTypes.NONE,
responses={
204: OpenApiResponse(description="Enrollment successful"),
400: OpenApiResponse(description="Enrollment pending/failed"),
420: OpenApiResponse(description="Enrollment pending/failed"),
},
)
@action(methods=["POST"], detail=True, permission_classes=[])
@ -57,10 +57,9 @@ class AuthenticatorDuoStageViewSet(ModelViewSet):
user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
status = client.enroll_status(user_id, activation_code)
print(status)
if status["response"] == "success":
if status == "success":
return Response(status=204)
return Response(status=400)
return Response(status=420)
class DuoDeviceSerializer(ModelSerializer):

View File

@ -36,24 +36,15 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
return AuthenticatorDuoStageView
_client: Optional[Auth] = None
@property
def client(self) -> Auth:
if not self._client:
self._client = Auth(
client = Auth(
self.client_id,
self.client_secret,
self.api_hostname,
user_agent=f"authentik {__version__}",
)
try:
self._client.ping()
except RuntimeError:
# Either allow login without 2FA, or abort the login process
# TODO: Define action when duo unavailable
raise
return self._client
return client
@property
def component(self) -> str:

View File

@ -1,13 +1,9 @@
"""Duo stage"""
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from duo_client.auth import Auth
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from rest_framework.fields import CharField
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
@ -18,7 +14,7 @@ SESSION_KEY_DUO_USER_ID = "authentik_stages_authenticator_duo_user_id"
SESSION_KEY_DUO_ACTIVATION_CODE = "authentik_stages_authenticator_duo_activation_code"
class AuthenticatorDuoChallenge(Challenge):
class AuthenticatorDuoChallenge(WithUserInfoChallenge):
"""Duo Challenge"""
activation_barcode = CharField()
@ -60,13 +56,12 @@ class AuthenticatorDuoStageView(ChallengeStageView):
stage: AuthenticatorDuoStage = self.executor.current_stage
user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
enroll_status = stage.client.enroll_status(user_id, activation_code).get(
"response"
)
enroll_status = stage.client.enroll_status(user_id, activation_code)
if enroll_status != "success":
# TODO: Find a better response
return HttpResponse(status=503)
return HttpResponse(status=420)
existing_device = DuoDevice.objects.filter(duo_user_id=user_id).first()
self.request.session.pop(SESSION_KEY_DUO_USER_ID)
self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE)
if not existing_device:
DuoDevice.objects.create(
user=self.get_pending_user(),

View File

@ -13,7 +13,7 @@ from webauthn.webauthn import (
)
from authentik.core.models import User
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
@ -32,7 +32,7 @@ SESSION_KEY_WEBAUTHN_AUTHENTICATED = (
)
class AuthenticatorWebAuthnChallenge(Challenge):
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
"""WebAuthn Challenge"""
registration = JSONField()

View File

@ -34,16 +34,19 @@ export class AuthenticatorDuoStage extends BaseStage {
firstUpdated(): void {
const i = setInterval(() => {
new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
this.checkEnrollStatus().then(() => {
clearInterval(i);
});
}, 3000);
}
checkEnrollStatus(): Promise<void> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stage_uuid || "",
}).then(r => {
console.log("success");
clearInterval(i);
this.host?.submit(new FormData());
this.host?.submit({});
}).catch(e => {
console.log("error");
});
}, 500);
}
render(): TemplateResult {
@ -75,8 +78,10 @@ export class AuthenticatorDuoStage extends BaseStage {
<a href=${this.challenge.activation_code}>${t`Duo activation`}</a>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
<button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.checkEnrollStatus();
}}>
${t`Check status`}
</button>
</div>
</form>

View File

@ -29,12 +29,6 @@ export class CaptchaStage extends BaseStage {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
submitFormAlt(token: string): void {
const form = new FormData();
form.set("token", token);
this.host?.submit(form);
}
firstUpdated(): void {
const script = document.createElement("script");
script.src = "https://www.google.com/recaptcha/api.js";
@ -50,7 +44,9 @@ export class CaptchaStage extends BaseStage {
const captchaId = grecaptcha.render(captchaContainer, {
sitekey: this.challenge.site_key,
callback: (token) => {
this.submitFormAlt(token);
this.host?.submit({
"token": token,
});
},
size: "invisible",
});