stages/authenticator_duo: improve setup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
9f5a3c396d
commit
65522186f1
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
});
|
||||
|
|
Reference in New Issue