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, request=OpenApiTypes.NONE,
responses={ responses={
204: OpenApiResponse(description="Enrollment successful"), 204: OpenApiResponse(description="Enrollment successful"),
400: OpenApiResponse(description="Enrollment pending/failed"), 420: OpenApiResponse(description="Enrollment pending/failed"),
}, },
) )
@action(methods=["POST"], detail=True, permission_classes=[]) @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) 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)
status = client.enroll_status(user_id, activation_code) status = client.enroll_status(user_id, activation_code)
print(status) if status == "success":
if status["response"] == "success":
return Response(status=204) return Response(status=204)
return Response(status=400) return Response(status=420)
class DuoDeviceSerializer(ModelSerializer): class DuoDeviceSerializer(ModelSerializer):

View File

@ -36,24 +36,15 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
return AuthenticatorDuoStageView return AuthenticatorDuoStageView
_client: Optional[Auth] = None
@property @property
def client(self) -> Auth: def client(self) -> Auth:
if not self._client: client = Auth(
self._client = Auth(
self.client_id, self.client_id,
self.client_secret, self.client_secret,
self.api_hostname, self.api_hostname,
user_agent=f"authentik {__version__}", user_agent=f"authentik {__version__}",
) )
try: return client
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
@property @property
def component(self) -> str: def component(self) -> str:

View File

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

View File

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

View File

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

View File

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