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,
|
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):
|
||||||
|
|
|
@ -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__}",
|
)
|
||||||
)
|
return client
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(() => {
|
||||||
stageUuid: this.challenge?.stage_uuid || "",
|
|
||||||
}).then(r => {
|
|
||||||
console.log("success");
|
|
||||||
clearInterval(i);
|
clearInterval(i);
|
||||||
this.host?.submit(new FormData());
|
|
||||||
}).catch(e => {
|
|
||||||
console.log("error");
|
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEnrollStatus(): Promise<void> {
|
||||||
|
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
|
||||||
|
stageUuid: this.challenge?.stage_uuid || "",
|
||||||
|
}).then(r => {
|
||||||
|
this.host?.submit({});
|
||||||
|
}).catch(e => {
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|
|
@ -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",
|
||||||
});
|
});
|
||||||
|
|
Reference in a new issue