stages/authenticator_duo: improve setup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -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 New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer