mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
stages/authenticator_webauthn: save attestation certificate when creating credential (#20095)
* stages/authenticator_webauthn: save attestation certificate when creating credential Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add toggle Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * gen Signed-off-by: Jens Langhammer <jens@goauthentik.io> * squash Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * gen Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@@ -26,6 +26,7 @@ class AuthenticatorWebAuthnStageSerializer(StageSerializer):
|
||||
"hints",
|
||||
"device_type_restrictions",
|
||||
"device_type_restrictions_obj",
|
||||
"prevent_duplicate_devices",
|
||||
"max_attempts",
|
||||
]
|
||||
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-24 13:00
|
||||
|
||||
import datetime
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
(
|
||||
"authentik_stages_authenticator_webauthn",
|
||||
"0012_webauthndevice_created_webauthndevice_last_updated_and_more",
|
||||
),
|
||||
("authentik_stages_authenticator_webauthn", "0013_authenticatorwebauthnstage_max_attempts"),
|
||||
(
|
||||
"authentik_stages_authenticator_webauthn",
|
||||
"0014_alter_authenticatorwebauthnstage_friendly_name",
|
||||
),
|
||||
("authentik_stages_authenticator_webauthn", "0015_authenticatorwebauthnstage_hints"),
|
||||
(
|
||||
"authentik_stages_authenticator_webauthn",
|
||||
"0016_authenticatorwebauthnstage_prevent_duplicate_devices_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_authenticator_webauthn", "0001_squashed_0011_webauthndevice_aaguid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="created",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="last_updated",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="last_used",
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authenticatorwebauthnstage",
|
||||
name="max_attempts",
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="authenticatorwebauthnstage",
|
||||
name="friendly_name",
|
||||
field=models.TextField(blank=True, default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authenticatorwebauthnstage",
|
||||
name="hints",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(
|
||||
choices=[
|
||||
("security-key", "Security Key"),
|
||||
("client-device", "Client Device"),
|
||||
("hybrid", "Hybrid"),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authenticatorwebauthnstage",
|
||||
name="prevent_duplicate_devices",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="When enabled, a given device can only be registered once."
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="attestation_certificate_fingerprint",
|
||||
field=models.TextField(default=None, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="attestation_certificate_pem",
|
||||
field=models.TextField(default=None, null=True),
|
||||
),
|
||||
]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-24 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_authenticator_webauthn", "0015_authenticatorwebauthnstage_hints"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authenticatorwebauthnstage",
|
||||
name="prevent_duplicate_devices",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="When enabled, a given device can only be registered once."
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="attestation_certificate_fingerprint",
|
||||
field=models.TextField(default=None, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="attestation_certificate_pem",
|
||||
field=models.TextField(default=None, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
"""WebAuthn stage"""
|
||||
|
||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.postgres.fields.array import ArrayField
|
||||
from django.db import models
|
||||
@@ -101,6 +102,9 @@ class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
choices=AuthenticatorAttachment.choices, default=None, null=True
|
||||
)
|
||||
|
||||
prevent_duplicate_devices = models.BooleanField(
|
||||
default=True, help_text=_("When enabled, a given device can only be registered once.")
|
||||
)
|
||||
hints = ArrayField(
|
||||
models.TextField(choices=WebAuthnHint.choices),
|
||||
default=list,
|
||||
@@ -159,6 +163,8 @@ class WebAuthnDevice(SerializerModel, Device):
|
||||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
last_t = models.DateTimeField(default=now)
|
||||
|
||||
attestation_certificate_pem = models.TextField(null=True, default=None)
|
||||
attestation_certificate_fingerprint = models.TextField(null=True, default=None)
|
||||
aaguid = models.TextField(default=UNKNOWN_DEVICE_TYPE_AAGUID)
|
||||
device_type = models.ForeignKey(
|
||||
"WebAuthnDeviceType", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||
@@ -169,6 +175,12 @@ class WebAuthnDevice(SerializerModel, Device):
|
||||
"""Get a publickeydescriptor for this device"""
|
||||
return PublicKeyCredentialDescriptor(id=base64url_to_bytes(self.credential_id))
|
||||
|
||||
@property
|
||||
def attestation_certificate(self) -> Certificate | None:
|
||||
if not self.attestation_certificate_pem:
|
||||
return None
|
||||
return load_pem_x509_certificate(self.attestation_certificate_pem.encode())
|
||||
|
||||
def set_sign_count(self, sign_count: int) -> None:
|
||||
"""Set the sign_count and update the last_t datetime."""
|
||||
self.sign_count = sign_count
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""WebAuthn stage"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.x509 import load_der_x509_certificate
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import gettext as __
|
||||
@@ -11,6 +15,7 @@ from rest_framework.serializers import ValidationError
|
||||
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
|
||||
from webauthn.helpers.exceptions import WebAuthnException
|
||||
from webauthn.helpers.options_to_json_dict import options_to_json_dict
|
||||
from webauthn.helpers.parse_attestation_object import parse_attestation_object
|
||||
from webauthn.helpers.structs import (
|
||||
AttestationConveyancePreference,
|
||||
AuthenticatorAttachment,
|
||||
@@ -28,6 +33,7 @@ from webauthn.registration.verify_registration_response import (
|
||||
|
||||
from authentik.core.api.utils import JSONDictField
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import fingerprint_sha256
|
||||
from authentik.flows.challenge import (
|
||||
Challenge,
|
||||
ChallengeResponse,
|
||||
@@ -46,6 +52,14 @@ PLAN_CONTEXT_WEBAUTHN_CHALLENGE = "goauthentik.io/stages/authenticator_webauthn/
|
||||
PLAN_CONTEXT_WEBAUTHN_ATTEMPT = "goauthentik.io/stages/authenticator_webauthn/attempt"
|
||||
|
||||
|
||||
@dataclass
|
||||
class VerifiedRegistrationData:
|
||||
registration: VerifiedRegistration
|
||||
exists_query: Q
|
||||
attest_cert: str | None = None
|
||||
attest_cert_fingerprint: str | None = None
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
|
||||
"""WebAuthn Challenge"""
|
||||
|
||||
@@ -62,7 +76,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
||||
request: HttpRequest
|
||||
user: User
|
||||
|
||||
def validate_response(self, response: dict) -> dict:
|
||||
def validate_response(self, response: dict) -> VerifiedRegistrationData:
|
||||
"""Validate webauthn challenge response"""
|
||||
challenge = self.stage.executor.plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE]
|
||||
|
||||
@@ -77,13 +91,33 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
||||
self.stage.logger.warning("registration failed", exc=exc)
|
||||
raise ValidationError(f"Registration failed. Error: {exc}") from None
|
||||
|
||||
credential_id_exists = WebAuthnDevice.objects.filter(
|
||||
credential_id=bytes_to_base64url(registration.credential_id)
|
||||
).first()
|
||||
registration_data = VerifiedRegistrationData(
|
||||
registration,
|
||||
exists_query=Q(credential_id=bytes_to_base64url(registration.credential_id)),
|
||||
)
|
||||
stage: AuthenticatorWebAuthnStage = self.stage.executor.current_stage
|
||||
|
||||
att_obj = parse_attestation_object(registration.attestation_object)
|
||||
if (
|
||||
att_obj
|
||||
and att_obj.att_stmt
|
||||
and att_obj.att_stmt.x5c is not None
|
||||
and len(att_obj.att_stmt.x5c) > 0
|
||||
):
|
||||
cert = load_der_x509_certificate(att_obj.att_stmt.x5c[0])
|
||||
registration_data.attest_cert = cert.public_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
).decode("utf-8")
|
||||
registration_data.attest_cert_fingerprint = fingerprint_sha256(cert)
|
||||
if stage.prevent_duplicate_devices:
|
||||
registration_data.exists_query |= Q(
|
||||
attestation_certificate_fingerprint=registration_data.attest_cert_fingerprint
|
||||
)
|
||||
|
||||
credential_id_exists = WebAuthnDevice.objects.filter(registration_data.exists_query).first()
|
||||
if credential_id_exists:
|
||||
raise ValidationError("Credential ID already exists.")
|
||||
|
||||
stage: AuthenticatorWebAuthnStage = self.stage.executor.current_stage
|
||||
aaguid = registration.aaguid
|
||||
allowed_aaguids = stage.device_type_restrictions.values_list("aaguid", flat=True)
|
||||
if allowed_aaguids.exists():
|
||||
@@ -103,11 +137,11 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
||||
UUID(UNKNOWN_DEVICE_TYPE_AAGUID) in allowed_aaguids
|
||||
and not WebAuthnDeviceType.objects.filter(aaguid=aaguid).exists()
|
||||
):
|
||||
return registration
|
||||
return registration_data
|
||||
# Otherwise just check if the given aaguid is in the allowed aaguids
|
||||
if UUID(aaguid) not in allowed_aaguids:
|
||||
raise invalid_error
|
||||
return registration
|
||||
return registration_data
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
@@ -190,26 +224,28 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
# Webauthn Challenge has already been validated
|
||||
webauthn_credential: VerifiedRegistration = response.validated_data["response"]
|
||||
existing_device = WebAuthnDevice.objects.filter(
|
||||
credential_id=bytes_to_base64url(webauthn_credential.credential_id)
|
||||
).first()
|
||||
webauthn_credential: VerifiedRegistrationData = response.validated_data["response"]
|
||||
existing_device = WebAuthnDevice.objects.filter(webauthn_credential.exists_query).first()
|
||||
if not existing_device:
|
||||
name = "WebAuthn Device"
|
||||
device_type = WebAuthnDeviceType.objects.filter(
|
||||
aaguid=webauthn_credential.aaguid
|
||||
aaguid=webauthn_credential.registration.aaguid
|
||||
).first()
|
||||
if device_type and device_type.description:
|
||||
name = device_type.description
|
||||
WebAuthnDevice.objects.create(
|
||||
name=name,
|
||||
user=self.get_pending_user(),
|
||||
public_key=bytes_to_base64url(webauthn_credential.credential_public_key),
|
||||
credential_id=bytes_to_base64url(webauthn_credential.credential_id),
|
||||
sign_count=webauthn_credential.sign_count,
|
||||
public_key=bytes_to_base64url(
|
||||
webauthn_credential.registration.credential_public_key
|
||||
),
|
||||
credential_id=bytes_to_base64url(webauthn_credential.registration.credential_id),
|
||||
sign_count=webauthn_credential.registration.sign_count,
|
||||
rp_id=get_rp_id(self.request),
|
||||
device_type=device_type,
|
||||
aaguid=webauthn_credential.aaguid,
|
||||
aaguid=webauthn_credential.registration.aaguid,
|
||||
attestation_certificate_pem=webauthn_credential.attest_cert,
|
||||
attestation_certificate_fingerprint=webauthn_credential.attest_cert_fingerprint,
|
||||
)
|
||||
else:
|
||||
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "f7wv8mP-poSxh-567eWxZntzCBDW8hWlvzf92QJkT--Y2oBRz4IEAZ6M2PI9_KEQ",
|
||||
"rawId": "f7wv8mP-poSxh-567eWxZntzCBDW8hWlvzf92QJkT--Y2oBRz4IEAZ6M2PI9_KEQ",
|
||||
"type": "public-key",
|
||||
"registrationClientExtensions": "{}",
|
||||
"response": {
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiaUhJWDNBdGtaWkN4U1lMeE9oazgwWlhJN1JuQUMwUGI0V1RrOWRFSjRlTEpkem9oOGpSbWpLVzJVOW9FX0NCbjVuNlpqNjdCSUladkZMM2xwaXdKd2ciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9",
|
||||
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgRkCRBg_Z0-cS8M4HyiZpar7cy6PRbGW_G0yTnG_lMUUCIHOKwNqU_Mr4sip5zUECezH-NJWdIGUbFR7D7mSC1wMSY3g1Y4FZAt0wggLZMIIBwaADAgECAgkA8Oq7fWgETIowDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG8xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDIxMDk0NjczNzYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATmZ9M7upxFm4Ce_MtqC64sXPxL14HVc0g9lv3pJR9kLM3mwgZVFPMzgkasmVKAACrSOK-8A3G21_rDv8ueedIwo4GBMH8wEwYKKwYBBAGCxAoNAQQFBAMFBAMwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQL8BXn4ETR-qxFrtajbkgKjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQC2Mago15M4rSkAig1_eaOgPc8uDJsfYvrPtIqeVZV3p1FslZtkKxjwDEx3Io0Z-dRCIlwSaL0jGKCMahdzBk8CmcmbskOKR7tnsdDbJSuUln4SAVqaK-nkLdRUJoiQYf4fIlb--Hbdc5kyRoNxGrBt6rxvRWhq-e7hgXlsIzs-2ew9wKy98vkNqE8ZJ-lz1jIA0bj05AE5miU0XcwEoquyk4AjtF9bQlJBjQ1SdYVjH2HEVs25iwoU3g1uUn9nP20yTVhhKRMnpV_EdOjm18hxot9nV0isx5jXb5Z6-My58Vb-oHgStjkaN-3dxuJkEQuZtD1AtTItfvyUeIsL2kkiaGF1dGhEYXRhWMJJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY8UAAAADL8BXn4ETR-qxFrtajbkgKgAwf7wv8mP-poSxh-567eWxZntzCBDW8hWlvzf92QJkT--Y2oBRz4IEAZ6M2PI9_KEQpQECAyYgASFYIH-8L_Jj_qaEsYfueu2KcYEacayeFjsZ1LowkryCG3MYIlggKCjYkvnPmx-ZcyOs3em0ZseMtwDga1j0Hi-WmFLboNmha2NyZWRQcm90ZWN0Ag"
|
||||
}
|
||||
}
|
||||
+13
-24
@@ -1,6 +1,7 @@
|
||||
"""Test WebAuthn API"""
|
||||
|
||||
from base64 import b64decode
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
|
||||
@@ -12,6 +13,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import load_fixture
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
UNKNOWN_DEVICE_TYPE_AAGUID,
|
||||
AuthenticatorWebAuthnStage,
|
||||
@@ -102,7 +104,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_WEBAUTHN_CHALLENGE] = b64decode(
|
||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||
b"iHIX3AtkZZCxSYLxOhk80ZXI7RnAC0Pb4WTk9dEJ4eLJdzoh8jRmjKW2U9oE/CBn5n6Zj67BIIZvFL3lpiwJwg=="
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@@ -111,35 +113,22 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
data={
|
||||
"component": "ak-stage-authenticator-webauthn",
|
||||
"response": {
|
||||
"id": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
|
||||
"rawId": "kqnmrVLnDG-OwsSNHkihYZaNz5s",
|
||||
"type": "public-key",
|
||||
"registrationClientExtensions": "{}",
|
||||
"response": {
|
||||
"clientDataJSON": (
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmd"
|
||||
"lIjoiMDNYb2RpNTRnS3NmblA1STlWRmZoYUdYVlZFMk5VeV"
|
||||
"pwQkJYbnNfSkkteDZWOVJZMlR3MlFteFJKa2hoNzE3NEVrU"
|
||||
"mF6VW50SXdqTVZZOWJGRzYwTHciLCJvcmlnaW4iOiJodHRw"
|
||||
"Oi8vbG9jYWxob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
|
||||
),
|
||||
"attestationObject": (
|
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYSZYN5Yg"
|
||||
"OjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NdAAAAAPv8MA"
|
||||
"cVTk7MjAtuAgVX170AFJKp5q1S5wxvjsLEjR5IoWGWjc-bp"
|
||||
"QECAyYgASFYIKtcZHPumH37XHs0IM1v3pUBRIqHVV_SE-Lq"
|
||||
"2zpJAOVXIlgg74Fg_WdB0kuLYqCKbxogkEPaVtR_iR3IyQFIJAXBzds"
|
||||
),
|
||||
},
|
||||
},
|
||||
"response": loads(load_fixture("fixtures/register.json")),
|
||||
},
|
||||
SERVER_NAME="localhost",
|
||||
SERVER_PORT="9000",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists())
|
||||
device = WebAuthnDevice.objects.filter(user=self.user).first()
|
||||
self.assertIsNotNone(device)
|
||||
self.assertEqual(
|
||||
device.credential_id, "f7wv8mP-poSxh-567eWxZntzCBDW8hWlvzf92QJkT--Y2oBRz4IEAZ6M2PI9_KEQ"
|
||||
)
|
||||
self.assertEqual(
|
||||
device.attestation_certificate_fingerprint,
|
||||
"3e:28:fc:df:45:19:bb:94:0a:0c:90:98:f2:08:72:53:2a:9e:e2:76:13:02:3e:69:61:4a:d9:90:49:80:3d:34",
|
||||
)
|
||||
|
||||
def test_register_restricted_device_type_deny(self):
|
||||
"""Test registration with restricted devices (fail)"""
|
||||
@@ -14854,6 +14854,11 @@
|
||||
},
|
||||
"title": "Device type restrictions"
|
||||
},
|
||||
"prevent_duplicate_devices": {
|
||||
"type": "boolean",
|
||||
"title": "Prevent duplicate devices",
|
||||
"description": "When enabled, a given device can only be registered once."
|
||||
},
|
||||
"max_attempts": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
|
||||
+40
-2
@@ -41,8 +41,10 @@ type AuthenticatorWebAuthnStage struct {
|
||||
Hints []WebAuthnHintEnum `json:"hints,omitempty"`
|
||||
DeviceTypeRestrictions []string `json:"device_type_restrictions,omitempty"`
|
||||
DeviceTypeRestrictionsObj []WebAuthnDeviceType `json:"device_type_restrictions_obj"`
|
||||
MaxAttempts *int32 `json:"max_attempts,omitempty"`
|
||||
AdditionalProperties map[string]interface{}
|
||||
// When enabled, a given device can only be registered once.
|
||||
PreventDuplicateDevices *bool `json:"prevent_duplicate_devices,omitempty"`
|
||||
MaxAttempts *int32 `json:"max_attempts,omitempty"`
|
||||
AdditionalProperties map[string]interface{}
|
||||
}
|
||||
|
||||
type _AuthenticatorWebAuthnStage AuthenticatorWebAuthnStage
|
||||
@@ -510,6 +512,38 @@ func (o *AuthenticatorWebAuthnStage) SetDeviceTypeRestrictionsObj(v []WebAuthnDe
|
||||
o.DeviceTypeRestrictionsObj = v
|
||||
}
|
||||
|
||||
// GetPreventDuplicateDevices returns the PreventDuplicateDevices field value if set, zero value otherwise.
|
||||
func (o *AuthenticatorWebAuthnStage) GetPreventDuplicateDevices() bool {
|
||||
if o == nil || IsNil(o.PreventDuplicateDevices) {
|
||||
var ret bool
|
||||
return ret
|
||||
}
|
||||
return *o.PreventDuplicateDevices
|
||||
}
|
||||
|
||||
// GetPreventDuplicateDevicesOk returns a tuple with the PreventDuplicateDevices field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *AuthenticatorWebAuthnStage) GetPreventDuplicateDevicesOk() (*bool, bool) {
|
||||
if o == nil || IsNil(o.PreventDuplicateDevices) {
|
||||
return nil, false
|
||||
}
|
||||
return o.PreventDuplicateDevices, true
|
||||
}
|
||||
|
||||
// HasPreventDuplicateDevices returns a boolean if a field has been set.
|
||||
func (o *AuthenticatorWebAuthnStage) HasPreventDuplicateDevices() bool {
|
||||
if o != nil && !IsNil(o.PreventDuplicateDevices) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetPreventDuplicateDevices gets a reference to the given bool and assigns it to the PreventDuplicateDevices field.
|
||||
func (o *AuthenticatorWebAuthnStage) SetPreventDuplicateDevices(v bool) {
|
||||
o.PreventDuplicateDevices = &v
|
||||
}
|
||||
|
||||
// GetMaxAttempts returns the MaxAttempts field value if set, zero value otherwise.
|
||||
func (o *AuthenticatorWebAuthnStage) GetMaxAttempts() int32 {
|
||||
if o == nil || IsNil(o.MaxAttempts) {
|
||||
@@ -581,6 +615,9 @@ func (o AuthenticatorWebAuthnStage) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize["device_type_restrictions"] = o.DeviceTypeRestrictions
|
||||
}
|
||||
toSerialize["device_type_restrictions_obj"] = o.DeviceTypeRestrictionsObj
|
||||
if !IsNil(o.PreventDuplicateDevices) {
|
||||
toSerialize["prevent_duplicate_devices"] = o.PreventDuplicateDevices
|
||||
}
|
||||
if !IsNil(o.MaxAttempts) {
|
||||
toSerialize["max_attempts"] = o.MaxAttempts
|
||||
}
|
||||
@@ -649,6 +686,7 @@ func (o *AuthenticatorWebAuthnStage) UnmarshalJSON(data []byte) (err error) {
|
||||
delete(additionalProperties, "hints")
|
||||
delete(additionalProperties, "device_type_restrictions")
|
||||
delete(additionalProperties, "device_type_restrictions_obj")
|
||||
delete(additionalProperties, "prevent_duplicate_devices")
|
||||
delete(additionalProperties, "max_attempts")
|
||||
o.AdditionalProperties = additionalProperties
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ type AuthenticatorWebAuthnStageRequest struct {
|
||||
ResidentKeyRequirement *UserVerificationEnum `json:"resident_key_requirement,omitempty"`
|
||||
Hints []WebAuthnHintEnum `json:"hints,omitempty"`
|
||||
DeviceTypeRestrictions []string `json:"device_type_restrictions,omitempty"`
|
||||
MaxAttempts *int32 `json:"max_attempts,omitempty"`
|
||||
// When enabled, a given device can only be registered once.
|
||||
PreventDuplicateDevices *bool `json:"prevent_duplicate_devices,omitempty"`
|
||||
MaxAttempts *int32 `json:"max_attempts,omitempty"`
|
||||
AdditionalProperties map[string]interface{}
|
||||
}
|
||||
|
||||
@@ -324,6 +326,38 @@ func (o *AuthenticatorWebAuthnStageRequest) SetDeviceTypeRestrictions(v []string
|
||||
o.DeviceTypeRestrictions = v
|
||||
}
|
||||
|
||||
// GetPreventDuplicateDevices returns the PreventDuplicateDevices field value if set, zero value otherwise.
|
||||
func (o *AuthenticatorWebAuthnStageRequest) GetPreventDuplicateDevices() bool {
|
||||
if o == nil || IsNil(o.PreventDuplicateDevices) {
|
||||
var ret bool
|
||||
return ret
|
||||
}
|
||||
return *o.PreventDuplicateDevices
|
||||
}
|
||||
|
||||
// GetPreventDuplicateDevicesOk returns a tuple with the PreventDuplicateDevices field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *AuthenticatorWebAuthnStageRequest) GetPreventDuplicateDevicesOk() (*bool, bool) {
|
||||
if o == nil || IsNil(o.PreventDuplicateDevices) {
|
||||
return nil, false
|
||||
}
|
||||
return o.PreventDuplicateDevices, true
|
||||
}
|
||||
|
||||
// HasPreventDuplicateDevices returns a boolean if a field has been set.
|
||||
func (o *AuthenticatorWebAuthnStageRequest) HasPreventDuplicateDevices() bool {
|
||||
if o != nil && !IsNil(o.PreventDuplicateDevices) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetPreventDuplicateDevices gets a reference to the given bool and assigns it to the PreventDuplicateDevices field.
|
||||
func (o *AuthenticatorWebAuthnStageRequest) SetPreventDuplicateDevices(v bool) {
|
||||
o.PreventDuplicateDevices = &v
|
||||
}
|
||||
|
||||
// GetMaxAttempts returns the MaxAttempts field value if set, zero value otherwise.
|
||||
func (o *AuthenticatorWebAuthnStageRequest) GetMaxAttempts() int32 {
|
||||
if o == nil || IsNil(o.MaxAttempts) {
|
||||
@@ -388,6 +422,9 @@ func (o AuthenticatorWebAuthnStageRequest) ToMap() (map[string]interface{}, erro
|
||||
if !IsNil(o.DeviceTypeRestrictions) {
|
||||
toSerialize["device_type_restrictions"] = o.DeviceTypeRestrictions
|
||||
}
|
||||
if !IsNil(o.PreventDuplicateDevices) {
|
||||
toSerialize["prevent_duplicate_devices"] = o.PreventDuplicateDevices
|
||||
}
|
||||
if !IsNil(o.MaxAttempts) {
|
||||
toSerialize["max_attempts"] = o.MaxAttempts
|
||||
}
|
||||
@@ -442,6 +479,7 @@ func (o *AuthenticatorWebAuthnStageRequest) UnmarshalJSON(data []byte) (err erro
|
||||
delete(additionalProperties, "resident_key_requirement")
|
||||
delete(additionalProperties, "hints")
|
||||
delete(additionalProperties, "device_type_restrictions")
|
||||
delete(additionalProperties, "prevent_duplicate_devices")
|
||||
delete(additionalProperties, "max_attempts")
|
||||
o.AdditionalProperties = additionalProperties
|
||||
}
|
||||
|
||||
+39
-1
@@ -29,7 +29,9 @@ type PatchedAuthenticatorWebAuthnStageRequest struct {
|
||||
ResidentKeyRequirement *UserVerificationEnum `json:"resident_key_requirement,omitempty"`
|
||||
Hints []WebAuthnHintEnum `json:"hints,omitempty"`
|
||||
DeviceTypeRestrictions []string `json:"device_type_restrictions,omitempty"`
|
||||
MaxAttempts *int32 `json:"max_attempts,omitempty"`
|
||||
// When enabled, a given device can only be registered once.
|
||||
PreventDuplicateDevices *bool `json:"prevent_duplicate_devices,omitempty"`
|
||||
MaxAttempts *int32 `json:"max_attempts,omitempty"`
|
||||
AdditionalProperties map[string]interface{}
|
||||
}
|
||||
|
||||
@@ -330,6 +332,38 @@ func (o *PatchedAuthenticatorWebAuthnStageRequest) SetDeviceTypeRestrictions(v [
|
||||
o.DeviceTypeRestrictions = v
|
||||
}
|
||||
|
||||
// GetPreventDuplicateDevices returns the PreventDuplicateDevices field value if set, zero value otherwise.
|
||||
func (o *PatchedAuthenticatorWebAuthnStageRequest) GetPreventDuplicateDevices() bool {
|
||||
if o == nil || IsNil(o.PreventDuplicateDevices) {
|
||||
var ret bool
|
||||
return ret
|
||||
}
|
||||
return *o.PreventDuplicateDevices
|
||||
}
|
||||
|
||||
// GetPreventDuplicateDevicesOk returns a tuple with the PreventDuplicateDevices field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *PatchedAuthenticatorWebAuthnStageRequest) GetPreventDuplicateDevicesOk() (*bool, bool) {
|
||||
if o == nil || IsNil(o.PreventDuplicateDevices) {
|
||||
return nil, false
|
||||
}
|
||||
return o.PreventDuplicateDevices, true
|
||||
}
|
||||
|
||||
// HasPreventDuplicateDevices returns a boolean if a field has been set.
|
||||
func (o *PatchedAuthenticatorWebAuthnStageRequest) HasPreventDuplicateDevices() bool {
|
||||
if o != nil && !IsNil(o.PreventDuplicateDevices) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetPreventDuplicateDevices gets a reference to the given bool and assigns it to the PreventDuplicateDevices field.
|
||||
func (o *PatchedAuthenticatorWebAuthnStageRequest) SetPreventDuplicateDevices(v bool) {
|
||||
o.PreventDuplicateDevices = &v
|
||||
}
|
||||
|
||||
// GetMaxAttempts returns the MaxAttempts field value if set, zero value otherwise.
|
||||
func (o *PatchedAuthenticatorWebAuthnStageRequest) GetMaxAttempts() int32 {
|
||||
if o == nil || IsNil(o.MaxAttempts) {
|
||||
@@ -396,6 +430,9 @@ func (o PatchedAuthenticatorWebAuthnStageRequest) ToMap() (map[string]interface{
|
||||
if !IsNil(o.DeviceTypeRestrictions) {
|
||||
toSerialize["device_type_restrictions"] = o.DeviceTypeRestrictions
|
||||
}
|
||||
if !IsNil(o.PreventDuplicateDevices) {
|
||||
toSerialize["prevent_duplicate_devices"] = o.PreventDuplicateDevices
|
||||
}
|
||||
if !IsNil(o.MaxAttempts) {
|
||||
toSerialize["max_attempts"] = o.MaxAttempts
|
||||
}
|
||||
@@ -429,6 +466,7 @@ func (o *PatchedAuthenticatorWebAuthnStageRequest) UnmarshalJSON(data []byte) (e
|
||||
delete(additionalProperties, "resident_key_requirement")
|
||||
delete(additionalProperties, "hints")
|
||||
delete(additionalProperties, "device_type_restrictions")
|
||||
delete(additionalProperties, "prevent_duplicate_devices")
|
||||
delete(additionalProperties, "max_attempts")
|
||||
o.AdditionalProperties = additionalProperties
|
||||
}
|
||||
|
||||
@@ -65,6 +65,12 @@ pub struct AuthenticatorWebAuthnStage {
|
||||
pub device_type_restrictions: Option<Vec<uuid::Uuid>>,
|
||||
#[serde(rename = "device_type_restrictions_obj")]
|
||||
pub device_type_restrictions_obj: Vec<models::WebAuthnDeviceType>,
|
||||
/// When enabled, a given device can only be registered once.
|
||||
#[serde(
|
||||
rename = "prevent_duplicate_devices",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub prevent_duplicate_devices: Option<bool>,
|
||||
#[serde(rename = "max_attempts", skip_serializing_if = "Option::is_none")]
|
||||
pub max_attempts: Option<u32>,
|
||||
}
|
||||
@@ -97,6 +103,7 @@ impl AuthenticatorWebAuthnStage {
|
||||
hints: None,
|
||||
device_type_restrictions: None,
|
||||
device_type_restrictions_obj,
|
||||
prevent_duplicate_devices: None,
|
||||
max_attempts: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ pub struct AuthenticatorWebAuthnStageRequest {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub device_type_restrictions: Option<Vec<uuid::Uuid>>,
|
||||
/// When enabled, a given device can only be registered once.
|
||||
#[serde(
|
||||
rename = "prevent_duplicate_devices",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub prevent_duplicate_devices: Option<bool>,
|
||||
#[serde(rename = "max_attempts", skip_serializing_if = "Option::is_none")]
|
||||
pub max_attempts: Option<u32>,
|
||||
}
|
||||
@@ -63,6 +69,7 @@ impl AuthenticatorWebAuthnStageRequest {
|
||||
resident_key_requirement: None,
|
||||
hints: None,
|
||||
device_type_restrictions: None,
|
||||
prevent_duplicate_devices: None,
|
||||
max_attempts: None,
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -47,6 +47,12 @@ pub struct PatchedAuthenticatorWebAuthnStageRequest {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub device_type_restrictions: Option<Vec<uuid::Uuid>>,
|
||||
/// When enabled, a given device can only be registered once.
|
||||
#[serde(
|
||||
rename = "prevent_duplicate_devices",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub prevent_duplicate_devices: Option<bool>,
|
||||
#[serde(rename = "max_attempts", skip_serializing_if = "Option::is_none")]
|
||||
pub max_attempts: Option<u32>,
|
||||
}
|
||||
@@ -63,6 +69,7 @@ impl PatchedAuthenticatorWebAuthnStageRequest {
|
||||
resident_key_requirement: None,
|
||||
hints: None,
|
||||
device_type_restrictions: None,
|
||||
prevent_duplicate_devices: None,
|
||||
max_attempts: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,12 @@ export interface AuthenticatorWebAuthnStage {
|
||||
* @memberof AuthenticatorWebAuthnStage
|
||||
*/
|
||||
readonly deviceTypeRestrictionsObj: Array<WebAuthnDeviceType>;
|
||||
/**
|
||||
* When enabled, a given device can only be registered once.
|
||||
* @type {boolean}
|
||||
* @memberof AuthenticatorWebAuthnStage
|
||||
*/
|
||||
preventDuplicateDevices?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
@@ -195,6 +201,7 @@ export function AuthenticatorWebAuthnStageFromJSONTyped(json: any, ignoreDiscrim
|
||||
'hints': json['hints'] == null ? undefined : ((json['hints'] as Array<any>).map(WebAuthnHintEnumFromJSON)),
|
||||
'deviceTypeRestrictions': json['device_type_restrictions'] == null ? undefined : json['device_type_restrictions'],
|
||||
'deviceTypeRestrictionsObj': ((json['device_type_restrictions_obj'] as Array<any>).map(WebAuthnDeviceTypeFromJSON)),
|
||||
'preventDuplicateDevices': json['prevent_duplicate_devices'] == null ? undefined : json['prevent_duplicate_devices'],
|
||||
'maxAttempts': json['max_attempts'] == null ? undefined : json['max_attempts'],
|
||||
};
|
||||
}
|
||||
@@ -218,6 +225,7 @@ export function AuthenticatorWebAuthnStageToJSONTyped(value?: Omit<Authenticator
|
||||
'resident_key_requirement': UserVerificationEnumToJSON(value['residentKeyRequirement']),
|
||||
'hints': value['hints'] == null ? undefined : ((value['hints'] as Array<any>).map(WebAuthnHintEnumToJSON)),
|
||||
'device_type_restrictions': value['deviceTypeRestrictions'],
|
||||
'prevent_duplicate_devices': value['preventDuplicateDevices'],
|
||||
'max_attempts': value['maxAttempts'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,6 +89,12 @@ export interface AuthenticatorWebAuthnStageRequest {
|
||||
* @memberof AuthenticatorWebAuthnStageRequest
|
||||
*/
|
||||
deviceTypeRestrictions?: Array<string>;
|
||||
/**
|
||||
* When enabled, a given device can only be registered once.
|
||||
* @type {boolean}
|
||||
* @memberof AuthenticatorWebAuthnStageRequest
|
||||
*/
|
||||
preventDuplicateDevices?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
@@ -125,6 +131,7 @@ export function AuthenticatorWebAuthnStageRequestFromJSONTyped(json: any, ignore
|
||||
'residentKeyRequirement': json['resident_key_requirement'] == null ? undefined : UserVerificationEnumFromJSON(json['resident_key_requirement']),
|
||||
'hints': json['hints'] == null ? undefined : ((json['hints'] as Array<any>).map(WebAuthnHintEnumFromJSON)),
|
||||
'deviceTypeRestrictions': json['device_type_restrictions'] == null ? undefined : json['device_type_restrictions'],
|
||||
'preventDuplicateDevices': json['prevent_duplicate_devices'] == null ? undefined : json['prevent_duplicate_devices'],
|
||||
'maxAttempts': json['max_attempts'] == null ? undefined : json['max_attempts'],
|
||||
};
|
||||
}
|
||||
@@ -148,6 +155,7 @@ export function AuthenticatorWebAuthnStageRequestToJSONTyped(value?: Authenticat
|
||||
'resident_key_requirement': UserVerificationEnumToJSON(value['residentKeyRequirement']),
|
||||
'hints': value['hints'] == null ? undefined : ((value['hints'] as Array<any>).map(WebAuthnHintEnumToJSON)),
|
||||
'device_type_restrictions': value['deviceTypeRestrictions'],
|
||||
'prevent_duplicate_devices': value['preventDuplicateDevices'],
|
||||
'max_attempts': value['maxAttempts'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,6 +89,12 @@ export interface PatchedAuthenticatorWebAuthnStageRequest {
|
||||
* @memberof PatchedAuthenticatorWebAuthnStageRequest
|
||||
*/
|
||||
deviceTypeRestrictions?: Array<string>;
|
||||
/**
|
||||
* When enabled, a given device can only be registered once.
|
||||
* @type {boolean}
|
||||
* @memberof PatchedAuthenticatorWebAuthnStageRequest
|
||||
*/
|
||||
preventDuplicateDevices?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
@@ -124,6 +130,7 @@ export function PatchedAuthenticatorWebAuthnStageRequestFromJSONTyped(json: any,
|
||||
'residentKeyRequirement': json['resident_key_requirement'] == null ? undefined : UserVerificationEnumFromJSON(json['resident_key_requirement']),
|
||||
'hints': json['hints'] == null ? undefined : ((json['hints'] as Array<any>).map(WebAuthnHintEnumFromJSON)),
|
||||
'deviceTypeRestrictions': json['device_type_restrictions'] == null ? undefined : json['device_type_restrictions'],
|
||||
'preventDuplicateDevices': json['prevent_duplicate_devices'] == null ? undefined : json['prevent_duplicate_devices'],
|
||||
'maxAttempts': json['max_attempts'] == null ? undefined : json['max_attempts'],
|
||||
};
|
||||
}
|
||||
@@ -147,6 +154,7 @@ export function PatchedAuthenticatorWebAuthnStageRequestToJSONTyped(value?: Patc
|
||||
'resident_key_requirement': UserVerificationEnumToJSON(value['residentKeyRequirement']),
|
||||
'hints': value['hints'] == null ? undefined : ((value['hints'] as Array<any>).map(WebAuthnHintEnumToJSON)),
|
||||
'device_type_restrictions': value['deviceTypeRestrictions'],
|
||||
'prevent_duplicate_devices': value['preventDuplicateDevices'],
|
||||
'max_attempts': value['maxAttempts'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35147,6 +35147,9 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/WebAuthnDeviceType'
|
||||
readOnly: true
|
||||
prevent_duplicate_devices:
|
||||
type: boolean
|
||||
description: When enabled, a given device can only be registered once.
|
||||
max_attempts:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -35192,6 +35195,9 @@ components:
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
prevent_duplicate_devices:
|
||||
type: boolean
|
||||
description: When enabled, a given device can only be registered once.
|
||||
max_attempts:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -47324,6 +47330,9 @@ components:
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
prevent_duplicate_devices:
|
||||
type: boolean
|
||||
description: When enabled, a given device can only be registered once.
|
||||
max_attempts:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
|
||||
@@ -3,6 +3,7 @@ import "#elements/ak-dual-select/ak-dual-select-provider";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#components/ak-switch-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -205,6 +206,14 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
|
||||
"Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.",
|
||||
)}
|
||||
></ak-number-input>
|
||||
<ak-switch-input
|
||||
name="preventDuplicateDevices"
|
||||
label=${msg("Prevent duplicate devices")}
|
||||
?checked=${this.instance?.preventDuplicateDevices ?? true}
|
||||
help=${msg(
|
||||
"When enabled, any unique authenticator can only be registered once.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Device type restrictions")}
|
||||
name="deviceTypeRestrictions"
|
||||
|
||||
@@ -22,6 +22,10 @@ Configure if the created authenticator is stored in the encrypted memory on the
|
||||
|
||||
Configure if authentik will require either a removable device (like a YubiKey, Google Titan, etc) or a non-removable device (like Windows Hello, TouchID or password managers), or not send a requirement.
|
||||
|
||||
### Prevent duplicate devices
|
||||
|
||||
When enabled, any unique authenticator can only be registered once. This check can only be enforced if the authenticator stores a unique attestsion certificate.
|
||||
|
||||
#### Device type restrictions
|
||||
|
||||
Optionally restrict the types of devices allowed to be enrolled. This option can be used to ensure users are only able to enroll FIPS-compliant devices for example.
|
||||
|
||||
Reference in New Issue
Block a user