stages/authenticator_validate: add Duo support
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
65522186f1
commit
3b41c662ed
|
@ -51,7 +51,9 @@ class AuthenticatorDuoStageViewSet(ModelViewSet):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@action(methods=["POST"], detail=True, permission_classes=[])
|
@action(methods=["POST"], detail=True, permission_classes=[])
|
||||||
|
# pylint: disable=invalid-name,unused-argument
|
||||||
def enrollment_status(self, request: Request, pk: str) -> Response:
|
def enrollment_status(self, request: Request, pk: str) -> Response:
|
||||||
|
"""Check enrollment status of user details in current session"""
|
||||||
stage: AuthenticatorDuoStage = self.get_object()
|
stage: AuthenticatorDuoStage = self.get_object()
|
||||||
client = stage.client
|
client = stage.client
|
||||||
user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
|
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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -15,45 +15,6 @@ class Migration(migrations.Migration):
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name="AuthenticatorDuoStage",
|
name="AuthenticatorDuoStage",
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -88,4 +49,50 @@ class Migration(migrations.Migration):
|
||||||
},
|
},
|
||||||
bases=("authentik_flows.stage", models.Model),
|
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.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
|
@ -38,6 +37,7 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self) -> Auth:
|
def client(self) -> Auth:
|
||||||
|
"""Get an API Client to talk to duo"""
|
||||||
client = Auth(
|
client = Auth(
|
||||||
self.client_id,
|
self.client_id,
|
||||||
self.client_secret,
|
self.client_secret,
|
||||||
|
@ -73,6 +73,8 @@ class DuoDevice(Device):
|
||||||
|
|
||||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
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()
|
duo_user_id = models.TextField()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -3,7 +3,12 @@ from django.http import HttpRequest, HttpResponse
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from structlog.stdlib import get_logger
|
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.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
|
||||||
|
@ -64,8 +69,7 @@ class AuthenticatorDuoStageView(ChallengeStageView):
|
||||||
self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE)
|
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(), duo_user_id=user_id, stage=stage
|
||||||
duo_user_id=user_id,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return self.executor.stage_invalid(
|
return self.executor.stage_invalid(
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
"""Validation stage challenge checking"""
|
"""Validation stage challenge checking"""
|
||||||
from django.http import HttpRequest
|
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.utils.translation import gettext_lazy as _
|
||||||
from django_otp import match_token
|
from django_otp import match_token
|
||||||
from django_otp.models import Device
|
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.fields import CharField, JSONField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
|
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
|
||||||
from webauthn.webauthn import (
|
from webauthn.webauthn import (
|
||||||
AuthenticationRejectedException,
|
AuthenticationRejectedException,
|
||||||
|
@ -16,9 +17,13 @@ from webauthn.webauthn import (
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import User
|
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.models import WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
|
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class DeviceChallenge(PassiveSerializer):
|
class DeviceChallenge(PassiveSerializer):
|
||||||
"""Single device challenge"""
|
"""Single device challenge"""
|
||||||
|
@ -30,10 +35,10 @@ class DeviceChallenge(PassiveSerializer):
|
||||||
|
|
||||||
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
|
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
|
||||||
"""Generate challenge for a single device"""
|
"""Generate challenge for a single device"""
|
||||||
if isinstance(device, (TOTPDevice, StaticDevice)):
|
if isinstance(device, WebAuthnDevice):
|
||||||
# Code-based challenges have no hints
|
return get_webauthn_challenge(request, device)
|
||||||
return {}
|
# Code-based challenges have no hints
|
||||||
return get_webauthn_challenge(request, device)
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict:
|
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)
|
device.set_sign_count(sign_count)
|
||||||
return data
|
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"
|
STATIC = "static"
|
||||||
TOTP = "totp", _("TOTP")
|
TOTP = "totp", _("TOTP")
|
||||||
WEBAUTHN = "webauthn", _("WebAuthn")
|
WEBAUTHN = "webauthn", _("WebAuthn")
|
||||||
|
DUO = "duo", _("Duo")
|
||||||
|
|
||||||
|
|
||||||
def default_device_classes() -> list:
|
def default_device_classes() -> list:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Authenticator Validation"""
|
"""Authenticator Validation"""
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django_otp import devices_for_user
|
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 rest_framework.serializers import ValidationError
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ from authentik.stages.authenticator_validate.challenge import (
|
||||||
DeviceChallenge,
|
DeviceChallenge,
|
||||||
get_challenge_for_device,
|
get_challenge_for_device,
|
||||||
validate_challenge_code,
|
validate_challenge_code,
|
||||||
|
validate_challenge_duo,
|
||||||
validate_challenge_webauthn,
|
validate_challenge_webauthn,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_validate.models import (
|
from authentik.stages.authenticator_validate.models import (
|
||||||
|
@ -40,17 +41,18 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
|
||||||
|
|
||||||
code = CharField(required=False)
|
code = CharField(required=False)
|
||||||
webauthn = JSONField(required=False)
|
webauthn = JSONField(required=False)
|
||||||
|
duo = IntegerField(required=False)
|
||||||
|
|
||||||
def validate_code(self, code: str) -> str:
|
def _challenge_allowed(self, classes: list):
|
||||||
"""Validate code-based response, raise error if code isn't allowed"""
|
|
||||||
device_challenges: list[dict] = self.stage.request.session.get(
|
device_challenges: list[dict] = self.stage.request.session.get(
|
||||||
"device_challenges"
|
"device_challenges"
|
||||||
)
|
)
|
||||||
if not any(
|
if not any(x["device_class"] in classes for x in device_challenges):
|
||||||
x["device_class"] in (DeviceClasses.TOTP, DeviceClasses.STATIC)
|
raise ValidationError("No compatible device class allowed")
|
||||||
for x in device_challenges
|
|
||||||
):
|
def validate_code(self, code: str) -> str:
|
||||||
raise ValidationError("Got code but no compatible device class allowed")
|
"""Validate code-based response, raise error if code isn't allowed"""
|
||||||
|
self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC])
|
||||||
return validate_challenge_code(
|
return validate_challenge_code(
|
||||||
code, self.stage.request, self.stage.get_pending_user()
|
code, self.stage.request, self.stage.get_pending_user()
|
||||||
)
|
)
|
||||||
|
@ -58,21 +60,22 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
|
||||||
def validate_webauthn(self, webauthn: dict) -> dict:
|
def validate_webauthn(self, webauthn: dict) -> dict:
|
||||||
"""Validate webauthn response, raise error if webauthn wasn't allowed
|
"""Validate webauthn response, raise error if webauthn wasn't allowed
|
||||||
or response is invalid"""
|
or response is invalid"""
|
||||||
device_challenges: list[dict] = self.stage.request.session.get(
|
self._challenge_allowed([DeviceClasses.WEBAUTHN])
|
||||||
"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")
|
|
||||||
return validate_challenge_webauthn(
|
return validate_challenge_webauthn(
|
||||||
webauthn, self.stage.request, self.stage.get_pending_user()
|
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):
|
def validate(self, data: dict):
|
||||||
# Checking if the given data is from a valid device class is done above
|
# 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
|
# 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")
|
raise ValidationError("Empty response")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,12 @@ from webauthn.webauthn import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from authentik.core.models import User
|
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.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
|
||||||
|
|
|
@ -10966,7 +10966,7 @@ paths:
|
||||||
/api/v2beta/stages/authenticator/duo/{stage_uuid}/enrollment_status/:
|
/api/v2beta/stages/authenticator/duo/{stage_uuid}/enrollment_status/:
|
||||||
post:
|
post:
|
||||||
operationId: stages_authenticator_duo_enrollment_status_create
|
operationId: stages_authenticator_duo_enrollment_status_create
|
||||||
description: AuthenticatorDuoStage Viewset
|
description: Check enrollment status of user details in current session
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: stage_uuid
|
name: stage_uuid
|
||||||
|
@ -10983,8 +10983,10 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: Enrollment successful
|
description: Enrollment successful
|
||||||
'400':
|
'420':
|
||||||
description: Enrollment pending/failed
|
description: Enrollment pending/failed
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
/api/v2beta/stages/authenticator/static/:
|
/api/v2beta/stages/authenticator/static/:
|
||||||
|
@ -15743,6 +15745,7 @@ components:
|
||||||
- static
|
- static
|
||||||
- totp
|
- totp
|
||||||
- webauthn
|
- webauthn
|
||||||
|
- duo
|
||||||
type: string
|
type: string
|
||||||
DigestAlgorithmEnum:
|
DigestAlgorithmEnum:
|
||||||
enum:
|
enum:
|
||||||
|
|
|
@ -11,12 +11,14 @@ import AKGlobal from "../../../authentik.css";
|
||||||
import { BaseStage, StageHost } from "../base";
|
import { BaseStage, StageHost } from "../base";
|
||||||
import "./AuthenticatorValidateStageWebAuthn";
|
import "./AuthenticatorValidateStageWebAuthn";
|
||||||
import "./AuthenticatorValidateStageCode";
|
import "./AuthenticatorValidateStageCode";
|
||||||
|
import "./AuthenticatorValidateStageDuo";
|
||||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
||||||
|
|
||||||
export enum DeviceClasses {
|
export enum DeviceClasses {
|
||||||
STATIC = "static",
|
STATIC = "static",
|
||||||
TOTP = "totp",
|
TOTP = "totp",
|
||||||
WEBAUTHN = "webauthn",
|
WEBAUTHN = "webauthn",
|
||||||
|
DUO = "duo",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceChallenge {
|
export interface DeviceChallenge {
|
||||||
|
@ -30,8 +32,9 @@ export interface AuthenticatorValidateStageChallenge extends WithUserInfoChallen
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticatorValidateStageChallengeResponse {
|
export interface AuthenticatorValidateStageChallengeResponse {
|
||||||
code: string;
|
code?: string;
|
||||||
webauthn: string;
|
webauthn?: string;
|
||||||
|
duo?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate")
|
@customElement("ak-stage-authenticator-validate")
|
||||||
|
@ -77,6 +80,12 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
||||||
|
|
||||||
renderDevicePickerSingle(deviceChallenge: DeviceChallenge): TemplateResult {
|
renderDevicePickerSingle(deviceChallenge: DeviceChallenge): TemplateResult {
|
||||||
switch (deviceChallenge.device_class) {
|
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:
|
case DeviceClasses.WEBAUTHN:
|
||||||
return html`<i class="fas fa-mobile-alt"></i>
|
return html`<i class="fas fa-mobile-alt"></i>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
@ -147,6 +156,13 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
||||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||||
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
|
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
|
||||||
</ak-stage-authenticator-validate-webauthn>`;
|
</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({
|
new FlowsApi(DEFAULT_CONFIG).flowsInstancesExecuteRetrieve({
|
||||||
slug: this.flow.slug
|
slug: this.flow.slug
|
||||||
}).then(link => {
|
}).then(link => {
|
||||||
const finalURL = `${link.link}?next=/%23${window.location.href}`;
|
const finalURL = `${link.link}?next=/%23${window.location.hash}`;
|
||||||
window.open(finalURL, "_blank");
|
window.open(finalURL, "_blank");
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
|
|
|
@ -91,9 +91,9 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
|
||||||
</select>
|
</select>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${t`User fields`}
|
label=${t`Device classes`}
|
||||||
?required=${true}
|
?required=${true}
|
||||||
name="transports">
|
name="deviceClasses">
|
||||||
<select name="users" class="pf-c-form-control" multiple>
|
<select name="users" class="pf-c-form-control" multiple>
|
||||||
<option value=${DeviceClassesEnum.Static} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Static)}>
|
<option value=${DeviceClassesEnum.Static} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Static)}>
|
||||||
${t`Static Tokens`}
|
${t`Static Tokens`}
|
||||||
|
@ -104,6 +104,9 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
|
||||||
<option value=${DeviceClassesEnum.Webauthn} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Webauthn)}>
|
<option value=${DeviceClassesEnum.Webauthn} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Webauthn)}>
|
||||||
${t`WebAuthn Authenticators`}
|
${t`WebAuthn Authenticators`}
|
||||||
</option>
|
</option>
|
||||||
|
<option value=${DeviceClassesEnum.Duo} ?selected=${this.isDeviceClassSelected(DeviceClassesEnum.Duo)}>
|
||||||
|
${t`Duo Authenticators`}
|
||||||
|
</option>
|
||||||
</select>
|
</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`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>
|
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
|
||||||
|
|
Reference in New Issue