stages/authenticator_validate: add Duo support
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		@ -51,7 +51,9 @@ class AuthenticatorDuoStageViewSet(ModelViewSet):
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    @action(methods=["POST"], detail=True, permission_classes=[])
 | 
			
		||||
    # pylint: disable=invalid-name,unused-argument
 | 
			
		||||
    def enrollment_status(self, request: Request, pk: str) -> Response:
 | 
			
		||||
        """Check enrollment status of user details in current session"""
 | 
			
		||||
        stage: AuthenticatorDuoStage = self.get_object()
 | 
			
		||||
        client = stage.client
 | 
			
		||||
        user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
# Generated by Django 3.2.3 on 2021-05-23 17:54
 | 
			
		||||
# Generated by Django 3.2.3 on 2021-05-23 20:28
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
@ -15,45 +15,6 @@ class Migration(migrations.Migration):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="DuoDevice",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.AutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "name",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        help_text="The human-readable name of this device.",
 | 
			
		||||
                        max_length=64,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "confirmed",
 | 
			
		||||
                    models.BooleanField(
 | 
			
		||||
                        default=True, help_text="Is this device ready for use?"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("duo_user_id", models.TextField()),
 | 
			
		||||
                (
 | 
			
		||||
                    "user",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Duo Device",
 | 
			
		||||
                "verbose_name_plural": "Duo Devices",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="AuthenticatorDuoStage",
 | 
			
		||||
            fields=[
 | 
			
		||||
@ -88,4 +49,50 @@ class Migration(migrations.Migration):
 | 
			
		||||
            },
 | 
			
		||||
            bases=("authentik_flows.stage", models.Model),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="DuoDevice",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.AutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "name",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        help_text="The human-readable name of this device.",
 | 
			
		||||
                        max_length=64,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "confirmed",
 | 
			
		||||
                    models.BooleanField(
 | 
			
		||||
                        default=True, help_text="Is this device ready for use?"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("duo_user_id", models.TextField()),
 | 
			
		||||
                (
 | 
			
		||||
                    "stage",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        to="authentik_stages_authenticator_duo.authenticatorduostage",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "user",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Duo Device",
 | 
			
		||||
                "verbose_name_plural": "Duo Devices",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ from typing import Optional, Type
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django.views import View
 | 
			
		||||
from django_otp.models import Device
 | 
			
		||||
@ -38,6 +37,7 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def client(self) -> Auth:
 | 
			
		||||
        """Get an API Client to talk to duo"""
 | 
			
		||||
        client = Auth(
 | 
			
		||||
            self.client_id,
 | 
			
		||||
            self.client_secret,
 | 
			
		||||
@ -73,6 +73,8 @@ class DuoDevice(Device):
 | 
			
		||||
 | 
			
		||||
    user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
 | 
			
		||||
 | 
			
		||||
    # Connect to the stage to when validating access we know the API Credentials
 | 
			
		||||
    stage = models.ForeignKey(AuthenticatorDuoStage, on_delete=models.CASCADE)
 | 
			
		||||
    duo_user_id = models.TextField()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,12 @@ from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from rest_framework.fields import CharField
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
 | 
			
		||||
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
 | 
			
		||||
@ -64,8 +69,7 @@ class AuthenticatorDuoStageView(ChallengeStageView):
 | 
			
		||||
        self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE)
 | 
			
		||||
        if not existing_device:
 | 
			
		||||
            DuoDevice.objects.create(
 | 
			
		||||
                user=self.get_pending_user(),
 | 
			
		||||
                duo_user_id=user_id,
 | 
			
		||||
                user=self.get_pending_user(), duo_user_id=user_id, stage=stage
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return self.executor.stage_invalid(
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
"""Validation stage challenge checking"""
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.http.response import Http404
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_otp import match_token
 | 
			
		||||
from django_otp.models import Device
 | 
			
		||||
from django_otp.plugins.otp_static.models import StaticDevice
 | 
			
		||||
from django_otp.plugins.otp_totp.models import TOTPDevice
 | 
			
		||||
from rest_framework.fields import CharField, JSONField
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
 | 
			
		||||
from webauthn.webauthn import (
 | 
			
		||||
    AuthenticationRejectedException,
 | 
			
		||||
@ -16,9 +17,13 @@ from webauthn.webauthn import (
 | 
			
		||||
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.lib.utils.http import get_client_ip
 | 
			
		||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
 | 
			
		||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
 | 
			
		||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeviceChallenge(PassiveSerializer):
 | 
			
		||||
    """Single device challenge"""
 | 
			
		||||
@ -30,10 +35,10 @@ class DeviceChallenge(PassiveSerializer):
 | 
			
		||||
 | 
			
		||||
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
 | 
			
		||||
    """Generate challenge for a single device"""
 | 
			
		||||
    if isinstance(device, (TOTPDevice, StaticDevice)):
 | 
			
		||||
        # Code-based challenges have no hints
 | 
			
		||||
        return {}
 | 
			
		||||
    return get_webauthn_challenge(request, device)
 | 
			
		||||
    if isinstance(device, WebAuthnDevice):
 | 
			
		||||
        return get_webauthn_challenge(request, device)
 | 
			
		||||
    # Code-based challenges have no hints
 | 
			
		||||
    return {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict:
 | 
			
		||||
@ -111,3 +116,24 @@ def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) ->
 | 
			
		||||
 | 
			
		||||
    device.set_sign_count(sign_count)
 | 
			
		||||
    return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_challenge_duo(device_pk: int, request: HttpRequest, user: User) -> int:
 | 
			
		||||
    """Duo authentication"""
 | 
			
		||||
    device = get_object_or_404(DuoDevice, pk=device_pk)
 | 
			
		||||
    if device.user != user:
 | 
			
		||||
        LOGGER.warning("device mismatch")
 | 
			
		||||
        raise Http404
 | 
			
		||||
    stage: AuthenticatorDuoStage = device.stage
 | 
			
		||||
    response = stage.client.auth(
 | 
			
		||||
        "auto",
 | 
			
		||||
        user_id=device.duo_user_id,
 | 
			
		||||
        ipaddr=get_client_ip(request),
 | 
			
		||||
        type="authentik Login request",
 | 
			
		||||
        display_username=user.username,
 | 
			
		||||
        device="auto",
 | 
			
		||||
    )
 | 
			
		||||
    # {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
 | 
			
		||||
    if response["result"] == "deny":
 | 
			
		||||
        raise ValidationError("Duo denied access")
 | 
			
		||||
    return device_pk
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ class DeviceClasses(models.TextChoices):
 | 
			
		||||
    STATIC = "static"
 | 
			
		||||
    TOTP = "totp", _("TOTP")
 | 
			
		||||
    WEBAUTHN = "webauthn", _("WebAuthn")
 | 
			
		||||
    DUO = "duo", _("Duo")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_device_classes() -> list:
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
"""Authenticator Validation"""
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django_otp import devices_for_user
 | 
			
		||||
from rest_framework.fields import CharField, JSONField, ListField
 | 
			
		||||
from rest_framework.fields import CharField, IntegerField, JSONField, ListField
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ from authentik.stages.authenticator_validate.challenge import (
 | 
			
		||||
    DeviceChallenge,
 | 
			
		||||
    get_challenge_for_device,
 | 
			
		||||
    validate_challenge_code,
 | 
			
		||||
    validate_challenge_duo,
 | 
			
		||||
    validate_challenge_webauthn,
 | 
			
		||||
)
 | 
			
		||||
from authentik.stages.authenticator_validate.models import (
 | 
			
		||||
@ -40,17 +41,18 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
 | 
			
		||||
 | 
			
		||||
    code = CharField(required=False)
 | 
			
		||||
    webauthn = JSONField(required=False)
 | 
			
		||||
    duo = IntegerField(required=False)
 | 
			
		||||
 | 
			
		||||
    def validate_code(self, code: str) -> str:
 | 
			
		||||
        """Validate code-based response, raise error if code isn't allowed"""
 | 
			
		||||
    def _challenge_allowed(self, classes: list):
 | 
			
		||||
        device_challenges: list[dict] = self.stage.request.session.get(
 | 
			
		||||
            "device_challenges"
 | 
			
		||||
        )
 | 
			
		||||
        if not any(
 | 
			
		||||
            x["device_class"] in (DeviceClasses.TOTP, DeviceClasses.STATIC)
 | 
			
		||||
            for x in device_challenges
 | 
			
		||||
        ):
 | 
			
		||||
            raise ValidationError("Got code but no compatible device class allowed")
 | 
			
		||||
        if not any(x["device_class"] in classes for x in device_challenges):
 | 
			
		||||
            raise ValidationError("No compatible device class allowed")
 | 
			
		||||
 | 
			
		||||
    def validate_code(self, code: str) -> str:
 | 
			
		||||
        """Validate code-based response, raise error if code isn't allowed"""
 | 
			
		||||
        self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC])
 | 
			
		||||
        return validate_challenge_code(
 | 
			
		||||
            code, self.stage.request, self.stage.get_pending_user()
 | 
			
		||||
        )
 | 
			
		||||
@ -58,21 +60,22 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
 | 
			
		||||
    def validate_webauthn(self, webauthn: dict) -> dict:
 | 
			
		||||
        """Validate webauthn response, raise error if webauthn wasn't allowed
 | 
			
		||||
        or response is invalid"""
 | 
			
		||||
        device_challenges: list[dict] = self.stage.request.session.get(
 | 
			
		||||
            "device_challenges"
 | 
			
		||||
        )
 | 
			
		||||
        if not any(
 | 
			
		||||
            x["device_class"] in (DeviceClasses.WEBAUTHN) for x in device_challenges
 | 
			
		||||
        ):
 | 
			
		||||
            raise ValidationError("Got webauthn but no compatible device class allowed")
 | 
			
		||||
        self._challenge_allowed([DeviceClasses.WEBAUTHN])
 | 
			
		||||
        return validate_challenge_webauthn(
 | 
			
		||||
            webauthn, self.stage.request, self.stage.get_pending_user()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate_duo(self, duo: int) -> int:
 | 
			
		||||
        """Initiate Duo authentication"""
 | 
			
		||||
        self._challenge_allowed([DeviceClasses.DUO])
 | 
			
		||||
        return validate_challenge_duo(
 | 
			
		||||
            duo, self.stage.request, self.stage.get_pending_user()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, data: dict):
 | 
			
		||||
        # Checking if the given data is from a valid device class is done above
 | 
			
		||||
        # Here we only check if the any data was sent at all
 | 
			
		||||
        if "code" not in data and "webauthn" not in data:
 | 
			
		||||
        if "code" not in data and "webauthn" not in data and "duo" not in data:
 | 
			
		||||
            raise ValidationError("Empty response")
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,12 @@ from webauthn.webauthn import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@ -10966,7 +10966,7 @@ paths:
 | 
			
		||||
  /api/v2beta/stages/authenticator/duo/{stage_uuid}/enrollment_status/:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: stages_authenticator_duo_enrollment_status_create
 | 
			
		||||
      description: AuthenticatorDuoStage Viewset
 | 
			
		||||
      description: Check enrollment status of user details in current session
 | 
			
		||||
      parameters:
 | 
			
		||||
      - in: path
 | 
			
		||||
        name: stage_uuid
 | 
			
		||||
@ -10983,8 +10983,10 @@ paths:
 | 
			
		||||
      responses:
 | 
			
		||||
        '204':
 | 
			
		||||
          description: Enrollment successful
 | 
			
		||||
        '400':
 | 
			
		||||
        '420':
 | 
			
		||||
          description: Enrollment pending/failed
 | 
			
		||||
        '400':
 | 
			
		||||
          $ref: '#/components/schemas/ValidationError'
 | 
			
		||||
        '403':
 | 
			
		||||
          $ref: '#/components/schemas/GenericError'
 | 
			
		||||
  /api/v2beta/stages/authenticator/static/:
 | 
			
		||||
@ -15743,6 +15745,7 @@ components:
 | 
			
		||||
      - static
 | 
			
		||||
      - totp
 | 
			
		||||
      - webauthn
 | 
			
		||||
      - duo
 | 
			
		||||
      type: string
 | 
			
		||||
    DigestAlgorithmEnum:
 | 
			
		||||
      enum:
 | 
			
		||||
 | 
			
		||||
@ -11,12 +11,14 @@ import AKGlobal from "../../../authentik.css";
 | 
			
		||||
import { BaseStage, StageHost } from "../base";
 | 
			
		||||
import "./AuthenticatorValidateStageWebAuthn";
 | 
			
		||||
import "./AuthenticatorValidateStageCode";
 | 
			
		||||
import "./AuthenticatorValidateStageDuo";
 | 
			
		||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
 | 
			
		||||
 | 
			
		||||
export enum DeviceClasses {
 | 
			
		||||
    STATIC = "static",
 | 
			
		||||
    TOTP = "totp",
 | 
			
		||||
    WEBAUTHN = "webauthn",
 | 
			
		||||
    DUO = "duo",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DeviceChallenge {
 | 
			
		||||
@ -30,8 +32,9 @@ export interface AuthenticatorValidateStageChallenge extends WithUserInfoChallen
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthenticatorValidateStageChallengeResponse {
 | 
			
		||||
    code: string;
 | 
			
		||||
    webauthn: string;
 | 
			
		||||
    code?: string;
 | 
			
		||||
    webauthn?: string;
 | 
			
		||||
    duo?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-validate")
 | 
			
		||||
@ -77,6 +80,12 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
 | 
			
		||||
 | 
			
		||||
    renderDevicePickerSingle(deviceChallenge: DeviceChallenge): TemplateResult {
 | 
			
		||||
        switch (deviceChallenge.device_class) {
 | 
			
		||||
            case DeviceClasses.DUO:
 | 
			
		||||
                return html`<i class="fas fa-mobile-alt"></i>
 | 
			
		||||
                    <div class="right">
 | 
			
		||||
                        <p>${t`Duo push-notifications`}</p>
 | 
			
		||||
                        <small>${t`Receive a push notification on your phone to prove your identity.`}</small>
 | 
			
		||||
                    </div>`;
 | 
			
		||||
            case DeviceClasses.WEBAUTHN:
 | 
			
		||||
                return html`<i class="fas fa-mobile-alt"></i>
 | 
			
		||||
                    <div class="right">
 | 
			
		||||
@ -147,6 +156,13 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
 | 
			
		||||
                .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                .showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
 | 
			
		||||
            </ak-stage-authenticator-validate-webauthn>`;
 | 
			
		||||
        case DeviceClasses.DUO:
 | 
			
		||||
            return html`<ak-stage-authenticator-validate-duo
 | 
			
		||||
                .host=${this}
 | 
			
		||||
                .challenge=${this.challenge}
 | 
			
		||||
                .deviceChallenge=${this.selectedDeviceChallenge}
 | 
			
		||||
                .showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
 | 
			
		||||
            </ak-stage-authenticator-validate-duo>`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
import { t } from "@lingui/macro";
 | 
			
		||||
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
 | 
			
		||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
 | 
			
		||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
import AKGlobal from "../../../authentik.css";
 | 
			
		||||
import { BaseStage } from "../base";
 | 
			
		||||
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
 | 
			
		||||
import "../../../elements/forms/FormElement";
 | 
			
		||||
import "../../../elements/EmptyState";
 | 
			
		||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
 | 
			
		||||
import "../../FormStatic";
 | 
			
		||||
import { FlowURLManager } from "../../../api/legacy";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-authenticator-validate-duo")
 | 
			
		||||
export class AuthenticatorValidateStageWebDuo extends BaseStage {
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    challenge?: AuthenticatorValidateStageChallenge;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    deviceChallenge?: DeviceChallenge;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    showBackButton = false;
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    firstUpdated(): void {
 | 
			
		||||
        this.host?.submit({
 | 
			
		||||
            "duo": this.deviceChallenge?.device_uid
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        if (!this.challenge) {
 | 
			
		||||
            return html`<ak-empty-state
 | 
			
		||||
                ?loading="${true}"
 | 
			
		||||
                header=${t`Loading`}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<div class="pf-c-login__main-body">
 | 
			
		||||
            <form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
 | 
			
		||||
                <ak-form-static
 | 
			
		||||
                    class="pf-c-form__group"
 | 
			
		||||
                    userAvatar="${this.challenge.pending_user_avatar}"
 | 
			
		||||
                    user=${this.challenge.pending_user}>
 | 
			
		||||
                    <div slot="link">
 | 
			
		||||
                        <a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </ak-form-static>
 | 
			
		||||
 | 
			
		||||
                <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>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
        <footer class="pf-c-login__main-footer">
 | 
			
		||||
            <ul class="pf-c-login__main-footer-links">
 | 
			
		||||
                ${this.showBackButton ?
 | 
			
		||||
                    html`<li class="pf-c-login__main-footer-links-item">
 | 
			
		||||
                        <button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
 | 
			
		||||
                            if (!this.host) return;
 | 
			
		||||
                            (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
 | 
			
		||||
                        }}>
 | 
			
		||||
                            ${t`Return to device picker`}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </li>`:
 | 
			
		||||
                    html``}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -74,7 +74,7 @@ export class FlowViewPage extends LitElement {
 | 
			
		||||
                                                    new FlowsApi(DEFAULT_CONFIG).flowsInstancesExecuteRetrieve({
 | 
			
		||||
                                                        slug: this.flow.slug
 | 
			
		||||
                                                    }).then(link => {
 | 
			
		||||
                                                        const finalURL = `${link.link}?next=/%23${window.location.href}`;
 | 
			
		||||
                                                        const finalURL = `${link.link}?next=/%23${window.location.hash}`;
 | 
			
		||||
                                                        window.open(finalURL, "_blank");
 | 
			
		||||
                                                    });
 | 
			
		||||
                                                }}>
 | 
			
		||||
 | 
			
		||||
@ -91,9 +91,9 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${t`User fields`}
 | 
			
		||||
                        label=${t`Device classes`}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                        name="transports">
 | 
			
		||||
                        name="deviceClasses">
 | 
			
		||||
                        <select name="users" class="pf-c-form-control" multiple>
 | 
			
		||||
                            <option value=${DeviceClassesEnum.Static} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Static)}>
 | 
			
		||||
                                ${t`Static Tokens`}
 | 
			
		||||
@ -104,6 +104,9 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
 | 
			
		||||
                            <option value=${DeviceClassesEnum.Webauthn} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Webauthn)}>
 | 
			
		||||
                                ${t`WebAuthn Authenticators`}
 | 
			
		||||
                            </option>
 | 
			
		||||
                            <option value=${DeviceClassesEnum.Duo} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Duo)}>
 | 
			
		||||
                                ${t`Duo Authenticators`}
 | 
			
		||||
                            </option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">${t`Device classes which can be used to authenticate.`}</p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user