mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
stages/authenticator_webauthn: add MDS support (#9114)
* web: align style to show current user for webauthn enroll Signed-off-by: Jens Langhammer <jens@goauthentik.io> * ask for aaguid Signed-off-by: Jens Langhammer <jens@goauthentik.io> * initial MDS import Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add API Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add restriction Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix api, add actual restriction Signed-off-by: Jens Langhammer <jens@goauthentik.io> * default authenticator name based on aaguid Signed-off-by: Jens Langhammer <jens@goauthentik.io> * connect device with device type Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix typo in webauthn stage name this typo has been around for 3 years https://github.com/goauthentik/authentik/commit/8708e487aebb0f131ee50af3a5088da927457941#diff-bb4aee4a37f4b95c8daa7beb6bf6251d8d2b6deb8c16dce0cd7cb0d6cd71900aR16 Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add fido2 dep Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add CI pipeline to automate updating blob Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests, include device type Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * exclude icon for now Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add passkeys aaguid Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make special unknown device type work, add docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
name: authentik-gen-update-webauthn-mds
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 1,15 * *'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- run: poetry run ak update_webauthn_mds
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: update-fido-mds-client
|
||||
commit-message: "stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs"
|
||||
title: "stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs"
|
||||
body: "stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
# ID from https://api.github.com/users/authentik-automation[bot]
|
||||
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
@@ -51,6 +51,7 @@ from authentik.policies.models import Policy, PolicyBindingModel
|
||||
from authentik.policies.reputation.models import Reputation
|
||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
# Context set when the serializer is created in a blueprint context
|
||||
@@ -95,6 +96,7 @@ def excluded_models() -> list[type[Model]]:
|
||||
AccessToken,
|
||||
RefreshToken,
|
||||
Reputation,
|
||||
WebAuthnDeviceType,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
|
||||
stage=stage_view.executor.current_stage,
|
||||
device=device,
|
||||
device_class=DeviceClasses.WEBAUTHN.value,
|
||||
device_type=device.device_type,
|
||||
)
|
||||
raise ValidationError("Assertion failed") from exc
|
||||
|
||||
|
||||
@@ -398,5 +398,6 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
self.executor.plan.context[PLAN_CONTEXT_METHOD] = "auth_webauthn_pwl"
|
||||
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS] = {
|
||||
"device": webauthn_device,
|
||||
"device_type": webauthn_device.device_type,
|
||||
}
|
||||
return self.set_valid_mfa_cookie(response.device)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""WebAuthnDeviceType API Views"""
|
||||
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
|
||||
|
||||
|
||||
class WebAuthnDeviceTypeSerializer(StageSerializer):
|
||||
"""WebAuthnDeviceType Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = WebAuthnDeviceType
|
||||
fields = [
|
||||
"aaguid",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
class WebAuthnDeviceTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""WebAuthnDeviceType Viewset"""
|
||||
|
||||
queryset = WebAuthnDeviceType.objects.all()
|
||||
serializer_class = WebAuthnDeviceTypeSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["description"]
|
||||
search_fields = ["description", "aaguid"]
|
||||
+6
-29
@@ -1,4 +1,4 @@
|
||||
"""AuthenticateWebAuthnStage API Views"""
|
||||
"""AuthenticatorWebAuthnStage API Views"""
|
||||
|
||||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||
from rest_framework import mixins
|
||||
@@ -9,41 +9,18 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageSerializer(StageSerializer):
|
||||
"""AuthenticateWebAuthnStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = AuthenticateWebAuthnStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"configure_flow",
|
||||
"friendly_name",
|
||||
"user_verification",
|
||||
"authenticator_attachment",
|
||||
"resident_key_requirement",
|
||||
]
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticateWebAuthnStage Viewset"""
|
||||
|
||||
queryset = AuthenticateWebAuthnStage.objects.all()
|
||||
serializer_class = AuthenticateWebAuthnStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
|
||||
|
||||
class WebAuthnDeviceSerializer(ModelSerializer):
|
||||
"""Serializer for WebAuthn authenticator devices"""
|
||||
|
||||
device_type = WebAuthnDeviceTypeSerializer(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = WebAuthnDevice
|
||||
fields = ["pk", "name", "created_on"]
|
||||
depth = 2
|
||||
fields = ["pk", "name", "created_on", "device_type"]
|
||||
|
||||
|
||||
class WebAuthnDeviceViewSet(
|
||||
@@ -0,0 +1,38 @@
|
||||
"""AuthenticatorWebAuthnStage API Views"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
|
||||
from authentik.stages.authenticator_webauthn.models import AuthenticatorWebAuthnStage
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnStageSerializer(StageSerializer):
|
||||
"""AuthenticatorWebAuthnStage Serializer"""
|
||||
|
||||
device_type_restrictions_obj = WebAuthnDeviceTypeSerializer(
|
||||
source="device_type_restrictions", many=True, read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AuthenticatorWebAuthnStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"configure_flow",
|
||||
"friendly_name",
|
||||
"user_verification",
|
||||
"authenticator_attachment",
|
||||
"resident_key_requirement",
|
||||
"device_type_restrictions",
|
||||
"device_type_restrictions_obj",
|
||||
]
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorWebAuthnStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorWebAuthnStage.objects.all()
|
||||
serializer_class = AuthenticatorWebAuthnStageSerializer
|
||||
filterset_fields = "__all__"
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
@@ -1,11 +1,22 @@
|
||||
"""authentik webauthn app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorWebAuthnConfig(AppConfig):
|
||||
class AuthentikStageAuthenticatorWebAuthnConfig(ManagedAppConfig):
|
||||
"""authentik webauthn config"""
|
||||
|
||||
name = "authentik.stages.authenticator_webauthn"
|
||||
label = "authentik_stages_authenticator_webauthn"
|
||||
verbose_name = "authentik Stages.Authenticator.WebAuthn"
|
||||
default = True
|
||||
|
||||
@ManagedAppConfig.reconcile_tenant
|
||||
def webauthn_device_types(self):
|
||||
from authentik.stages.authenticator_webauthn.tasks import (
|
||||
webauthn_aaguid_import,
|
||||
webauthn_mds_import,
|
||||
)
|
||||
|
||||
webauthn_mds_import.delay()
|
||||
webauthn_aaguid_import.delay()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from fido2.mds3 import parse_blob
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.stages.authenticator_webauthn.tasks import AAGUID_BLOB_PATH, MDS_BLOB_PATH, mds_ca
|
||||
|
||||
MDS3_URL = "https://mds3.fidoalliance.org/"
|
||||
AAGUID_URL = "https://passkeydeveloper.github.io/passkey-authenticator-aaguids/aaguid.json"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Update FIDO Alliances' MDS3 blob and validate it."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logger = get_logger()
|
||||
|
||||
def update_fido_mds(self):
|
||||
with open(MDS_BLOB_PATH, "w", encoding="utf-8") as _raw_file:
|
||||
_raw_file.write(get_http_session().get(MDS3_URL).text)
|
||||
self.logger.info("Updated MDS blob")
|
||||
with open(MDS_BLOB_PATH, mode="rb") as _raw_blob:
|
||||
parse_blob(_raw_blob.read(), mds_ca())
|
||||
self.logger.info("Successfully validated MDS blob")
|
||||
|
||||
def update_passkey_aaguids(self):
|
||||
with open(AAGUID_BLOB_PATH, "w", encoding="utf-8") as _raw_file:
|
||||
_raw_file.write(get_http_session().get(AAGUID_URL).text)
|
||||
self.logger.info("Updated AAGUID blob")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.update_fido_mds()
|
||||
self.update_passkey_aaguids()
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+45
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.0.3 on 2024-04-02 23:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0027_auto_20231028_1424"),
|
||||
("authentik_stages_authenticator_webauthn", "0009_authenticatewebauthnstage_friendly_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="WebAuthnDeviceType",
|
||||
fields=[
|
||||
("aaguid", models.UUIDField(primary_key=True, serialize=False, unique=True)),
|
||||
("description", models.TextField()),
|
||||
("icon", models.TextField(null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "WebAuthn Device type",
|
||||
"verbose_name_plural": "WebAuthn Device types",
|
||||
},
|
||||
),
|
||||
migrations.RenameModel("AuthenticateWebAuthnStage", "AuthenticatorWebAuthnStage"),
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="device_type",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_stages_authenticator_webauthn.webauthndevicetype",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authenticatorwebauthnstage",
|
||||
name="device_type_restrictions",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="authentik_stages_authenticator_webauthn.webauthndevicetype"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -14,6 +14,8 @@ from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.stages.authenticator.models import Device
|
||||
|
||||
UNKNOWN_DEVICE_TYPE_AAGUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
class UserVerification(models.TextChoices):
|
||||
"""The degree to which the Relying Party wishes to verify a user's identity.
|
||||
@@ -65,7 +67,7 @@ class AuthenticatorAttachment(models.TextChoices):
|
||||
CROSS_PLATFORM = "cross-platform"
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""WebAuthn stage"""
|
||||
|
||||
user_verification = models.TextField(
|
||||
@@ -80,11 +82,15 @@ class AuthenticateWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
choices=AuthenticatorAttachment.choices, default=None, null=True
|
||||
)
|
||||
|
||||
device_type_restrictions = models.ManyToManyField("WebAuthnDeviceType", blank=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageSerializer
|
||||
from authentik.stages.authenticator_webauthn.api.stages import (
|
||||
AuthenticatorWebAuthnStageSerializer,
|
||||
)
|
||||
|
||||
return AuthenticateWebAuthnStageSerializer
|
||||
return AuthenticatorWebAuthnStageSerializer
|
||||
|
||||
@property
|
||||
def view(self) -> type[View]:
|
||||
@@ -126,6 +132,10 @@ class WebAuthnDevice(SerializerModel, Device):
|
||||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
last_t = models.DateTimeField(default=now)
|
||||
|
||||
device_type = models.ForeignKey(
|
||||
"WebAuthnDeviceType", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||
)
|
||||
|
||||
@property
|
||||
def descriptor(self) -> PublicKeyCredentialDescriptor:
|
||||
"""Get a publickeydescriptor for this device"""
|
||||
@@ -139,7 +149,7 @@ class WebAuthnDevice(SerializerModel, Device):
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.stages.authenticator_webauthn.api import WebAuthnDeviceSerializer
|
||||
from authentik.stages.authenticator_webauthn.api.devices import WebAuthnDeviceSerializer
|
||||
|
||||
return WebAuthnDeviceSerializer
|
||||
|
||||
@@ -149,3 +159,27 @@ class WebAuthnDevice(SerializerModel, Device):
|
||||
class Meta:
|
||||
verbose_name = _("WebAuthn Device")
|
||||
verbose_name_plural = _("WebAuthn Devices")
|
||||
|
||||
|
||||
class WebAuthnDeviceType(SerializerModel):
|
||||
"""WebAuthn device type, used to restrict which device types are allowed"""
|
||||
|
||||
aaguid = models.UUIDField(primary_key=True, unique=True)
|
||||
|
||||
description = models.TextField()
|
||||
icon = models.TextField(null=True)
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.stages.authenticator_webauthn.api.device_types import (
|
||||
WebAuthnDeviceTypeSerializer,
|
||||
)
|
||||
|
||||
return WebAuthnDeviceTypeSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WebAuthn Device type")
|
||||
verbose_name_plural = _("WebAuthn Device types")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"WebAuthn device type {self.description} ({self.aaguid})"
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
"""WebAuthn stage"""
|
||||
|
||||
from json import loads
|
||||
from uuid import UUID
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
from webauthn import options_to_json
|
||||
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
|
||||
from webauthn.helpers.exceptions import InvalidRegistrationResponse
|
||||
from webauthn.helpers.structs import (
|
||||
AttestationConveyancePreference,
|
||||
AuthenticatorAttachment,
|
||||
AuthenticatorSelectionCriteria,
|
||||
PublicKeyCredentialCreationOptions,
|
||||
@@ -31,7 +34,12 @@ from authentik.flows.challenge import (
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
UNKNOWN_DEVICE_TYPE_AAGUID,
|
||||
AuthenticatorWebAuthnStage,
|
||||
WebAuthnDevice,
|
||||
WebAuthnDeviceType,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
||||
|
||||
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
|
||||
@@ -74,6 +82,30 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
||||
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():
|
||||
invalid_error = ValidationError(
|
||||
_(
|
||||
"Invalid device type. Contact your {brand} administrator for help.".format(
|
||||
brand=self.stage.request.brand.branding_title
|
||||
)
|
||||
)
|
||||
)
|
||||
# If there are any restrictions set and we didn't get an aaguid, invalid
|
||||
if not aaguid:
|
||||
raise invalid_error
|
||||
# If one of the restrictions is the "special" unknown device type UUID
|
||||
# but we do have a device type for the given aaguid, invalid
|
||||
if (
|
||||
UUID(UNKNOWN_DEVICE_TYPE_AAGUID) in allowed_aaguids
|
||||
and not WebAuthnDeviceType.objects.filter(aaguid=aaguid).exists()
|
||||
):
|
||||
return registration
|
||||
# Otherwise just check if the given aaguid is in the allowed aaguids
|
||||
if UUID(aaguid) not in allowed_aaguids:
|
||||
raise invalid_error
|
||||
return registration
|
||||
|
||||
|
||||
@@ -85,7 +117,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
# clear session variables prior to starting a new registration
|
||||
self.request.session.pop(SESSION_KEY_WEBAUTHN_CHALLENGE, None)
|
||||
stage: AuthenticateWebAuthnStage = self.executor.current_stage
|
||||
stage: AuthenticatorWebAuthnStage = self.executor.current_stage
|
||||
user = self.get_pending_user()
|
||||
|
||||
# library accepts none so we store null in the database, but if there is a value
|
||||
@@ -105,6 +137,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
user_verification=UserVerificationRequirement(str(stage.user_verification)),
|
||||
authenticator_attachment=authenticator_attachment,
|
||||
),
|
||||
attestation=AttestationConveyancePreference.DIRECT,
|
||||
)
|
||||
|
||||
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge
|
||||
@@ -129,13 +162,20 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
credential_id=bytes_to_base64url(webauthn_credential.credential_id)
|
||||
).first()
|
||||
if not existing_device:
|
||||
name = "WebAuthn Device"
|
||||
device_type = WebAuthnDeviceType.objects.filter(
|
||||
aaguid=webauthn_credential.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,
|
||||
rp_id=get_rp_id(self.request),
|
||||
name="WebAuthn Device",
|
||||
device_type=device_type,
|
||||
)
|
||||
else:
|
||||
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""MDS Helpers"""
|
||||
|
||||
from functools import lru_cache
|
||||
from json import loads
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.transaction import atomic
|
||||
from fido2.mds3 import filter_revoked, parse_blob
|
||||
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
UNKNOWN_DEVICE_TYPE_AAGUID,
|
||||
WebAuthnDeviceType,
|
||||
)
|
||||
|
||||
CACHE_KEY_MDS_NO = "goauthentik.io/stages/authenticator_webauthn/mds_no"
|
||||
AAGUID_BLOB_PATH = Path(__file__).parent / "mds" / "aaguid.json"
|
||||
MDS_BLOB_PATH = Path(__file__).parent / "mds" / "blob.jwt"
|
||||
MDS_CA_PATH = Path(__file__).parent / "mds" / "root-r3.crt"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def mds_ca() -> bytes:
|
||||
"""Cache MDS Signature CA, GlobalSign Root CA - R3"""
|
||||
with open(MDS_CA_PATH, mode="rb") as _raw_root:
|
||||
return _raw_root.read()
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def webauthn_mds_import(force=False):
|
||||
"""Background task to import FIDO Alliance MDS blob into database"""
|
||||
with open(MDS_BLOB_PATH, mode="rb") as _raw_blob:
|
||||
blob = parse_blob(_raw_blob.read(), mds_ca())
|
||||
with atomic():
|
||||
WebAuthnDeviceType.objects.update_or_create(
|
||||
aaguid=UNKNOWN_DEVICE_TYPE_AAGUID,
|
||||
defaults={
|
||||
"description": "authentik: Unknown devices",
|
||||
},
|
||||
)
|
||||
if cache.get(CACHE_KEY_MDS_NO) == blob.no and not force:
|
||||
return
|
||||
for entry in blob.entries:
|
||||
aaguid = entry.aaguid
|
||||
if not aaguid:
|
||||
continue
|
||||
if not filter_revoked(entry):
|
||||
WebAuthnDeviceType.objects.filter(aaguid=str(aaguid)).delete()
|
||||
continue
|
||||
metadata = entry.metadata_statement
|
||||
WebAuthnDeviceType.objects.update_or_create(
|
||||
aaguid=str(aaguid),
|
||||
defaults={"description": metadata.description, "icon": metadata.icon},
|
||||
)
|
||||
cache.set(CACHE_KEY_MDS_NO, blob.no)
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def webauthn_aaguid_import(force=False):
|
||||
"""Background task to import AAGUIDs into database"""
|
||||
with open(AAGUID_BLOB_PATH, mode="rb") as _raw_blob:
|
||||
entries = loads(_raw_blob.read())
|
||||
with atomic():
|
||||
for aaguid, details in entries.items():
|
||||
WebAuthnDeviceType.objects.update_or_create(
|
||||
aaguid=str(aaguid),
|
||||
defaults={"description": details.get("name"), "icon": details.get("icon_light")},
|
||||
)
|
||||
@@ -12,15 +12,24 @@ 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.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
UNKNOWN_DEVICE_TYPE_AAGUID,
|
||||
AuthenticatorWebAuthnStage,
|
||||
WebAuthnDevice,
|
||||
WebAuthnDeviceType,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
||||
from authentik.stages.authenticator_webauthn.tasks import (
|
||||
webauthn_aaguid_import,
|
||||
webauthn_mds_import,
|
||||
)
|
||||
|
||||
|
||||
class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
||||
"""Test WebAuthn API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.stage = AuthenticateWebAuthnStage.objects.create(
|
||||
self.stage = AuthenticatorWebAuthnStage.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
self.flow = create_test_flow()
|
||||
@@ -46,10 +55,6 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
||||
b"o90Yh1osqW3mjGift+6WclWOya5lcdff/G0mqueN3hChacMUz"
|
||||
b"V4mxiDafuQ0x0e1d/fcPai0fx/jMBZ8/nG2qQ=="
|
||||
)
|
||||
session.save()
|
||||
|
||||
response = self.client.get(
|
||||
@@ -87,6 +92,218 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
||||
"requireResidentKey": False,
|
||||
"userVerification": "preferred",
|
||||
},
|
||||
"attestation": "none",
|
||||
"attestation": "direct",
|
||||
},
|
||||
)
|
||||
|
||||
def test_register(self):
|
||||
"""Test registration"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||
)
|
||||
session.save()
|
||||
response = self.client.post(
|
||||
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"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
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())
|
||||
|
||||
def test_register_restricted_device_type_deny(self):
|
||||
"""Test registration with restricted devices (fail)"""
|
||||
webauthn_mds_import(force=True)
|
||||
webauthn_aaguid_import()
|
||||
self.stage.device_type_restrictions.set(
|
||||
WebAuthnDeviceType.objects.filter(
|
||||
description="Android Authenticator with SafetyNet Attestation"
|
||||
)
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||
)
|
||||
session.save()
|
||||
response = self.client.post(
|
||||
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"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
SERVER_NAME="localhost",
|
||||
SERVER_PORT="9000",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
flow=self.flow,
|
||||
component="ak-stage-authenticator-webauthn",
|
||||
response_errors={
|
||||
"response": [
|
||||
{
|
||||
"string": (
|
||||
"Invalid device type. Contact your authentik administrator for help."
|
||||
),
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
self.assertFalse(WebAuthnDevice.objects.filter(user=self.user).exists())
|
||||
|
||||
def test_register_restricted_device_type_allow(self):
|
||||
"""Test registration with restricted devices (allow)"""
|
||||
webauthn_mds_import(force=True)
|
||||
webauthn_aaguid_import()
|
||||
self.stage.device_type_restrictions.set(
|
||||
WebAuthnDeviceType.objects.filter(description="iCloud Keychain")
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||
)
|
||||
session.save()
|
||||
response = self.client.post(
|
||||
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"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
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())
|
||||
|
||||
def test_register_restricted_device_type_allow_unknown(self):
|
||||
"""Test registration with restricted devices (allow, unknown device type)"""
|
||||
webauthn_mds_import(force=True)
|
||||
webauthn_aaguid_import()
|
||||
WebAuthnDeviceType.objects.filter(description="iCloud Keychain").delete()
|
||||
self.stage.device_type_restrictions.set(
|
||||
WebAuthnDeviceType.objects.filter(aaguid=UNKNOWN_DEVICE_TYPE_AAGUID)
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = b64decode(
|
||||
b"03Xodi54gKsfnP5I9VFfhaGXVVE2NUyZpBBXns/JI+x6V9RY2Tw2QmxRJkhh7174EkRazUntIwjMVY9bFG60Lw=="
|
||||
)
|
||||
session.save()
|
||||
response = self.client.post(
|
||||
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"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
SERVER_NAME="localhost",
|
||||
SERVER_PORT="9000",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
print(response.content)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
self.assertTrue(WebAuthnDevice.objects.filter(user=self.user).exists())
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.stages.authenticator_webauthn.api import (
|
||||
AuthenticateWebAuthnStageViewSet,
|
||||
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeViewSet
|
||||
from authentik.stages.authenticator_webauthn.api.devices import (
|
||||
WebAuthnAdminDeviceViewSet,
|
||||
WebAuthnDeviceViewSet,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.api.stages import AuthenticatorWebAuthnStageViewSet
|
||||
|
||||
api_urlpatterns = [
|
||||
("stages/authenticator/webauthn", AuthenticateWebAuthnStageViewSet),
|
||||
("stages/authenticator/webauthn", AuthenticatorWebAuthnStageViewSet),
|
||||
("stages/authenticator/webauthn_device_types", WebAuthnDeviceTypeViewSet),
|
||||
(
|
||||
"authenticators/admin/webauthn",
|
||||
WebAuthnAdminDeviceViewSet,
|
||||
|
||||
@@ -17,7 +17,7 @@ entries:
|
||||
identifiers:
|
||||
name: default-authenticator-webauthn-setup
|
||||
id: default-authenticator-webauthn-setup
|
||||
model: authentik_stages_authenticator_webauthn.authenticatewebauthnstage
|
||||
model: authentik_stages_authenticator_webauthn.authenticatorwebauthnstage
|
||||
- identifiers:
|
||||
order: 0
|
||||
stage: !KeyOf default-authenticator-webauthn-setup
|
||||
|
||||
+12
-5
@@ -1566,7 +1566,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_stages_authenticator_webauthn.authenticatewebauthnstage"
|
||||
"const": "authentik_stages_authenticator_webauthn.authenticatorwebauthnstage"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
@@ -1588,10 +1588,10 @@
|
||||
}
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_webauthn.authenticatewebauthnstage"
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_webauthn.authenticatorwebauthnstage"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_webauthn.authenticatewebauthnstage"
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_webauthn.authenticatorwebauthnstage"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3354,7 +3354,7 @@
|
||||
"authentik_stages_authenticator_totp.authenticatortotpstage",
|
||||
"authentik_stages_authenticator_totp.totpdevice",
|
||||
"authentik_stages_authenticator_validate.authenticatorvalidatestage",
|
||||
"authentik_stages_authenticator_webauthn.authenticatewebauthnstage",
|
||||
"authentik_stages_authenticator_webauthn.authenticatorwebauthnstage",
|
||||
"authentik_stages_authenticator_webauthn.webauthndevice",
|
||||
"authentik_stages_captcha.captchastage",
|
||||
"authentik_stages_consent.consentstage",
|
||||
@@ -5645,7 +5645,7 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_authenticator_webauthn.authenticatewebauthnstage": {
|
||||
"model_authentik_stages_authenticator_webauthn.authenticatorwebauthnstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -5778,6 +5778,13 @@
|
||||
"required"
|
||||
],
|
||||
"title": "Resident key requirement"
|
||||
},
|
||||
"device_type_restrictions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": "Device type restrictions"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
||||
Generated
+18
-1
@@ -1431,6 +1431,23 @@ files = [
|
||||
[package.dependencies]
|
||||
requests = "*"
|
||||
|
||||
[[package]]
|
||||
name = "fido2"
|
||||
version = "1.1.3"
|
||||
description = "FIDO2/WebAuthn library for implementing clients and servers."
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
files = [
|
||||
{file = "fido2-1.1.3-py3-none-any.whl", hash = "sha256:6be34c0b9fe85e4911fd2d103cce7ae8ce2f064384a7a2a3bd970b3ef7702931"},
|
||||
{file = "fido2-1.1.3.tar.gz", hash = "sha256:26100f226d12ced621ca6198528ce17edf67b78df4287aee1285fee3cd5aa9fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=2.6,<35 || >35,<45"
|
||||
|
||||
[package.extras]
|
||||
pcsc = ["pyscard (>=1.9,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "flower"
|
||||
version = "2.0.1"
|
||||
@@ -4636,4 +4653,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12"
|
||||
content-hash = "c5a36b528980277b07f80200da251a2bea31cc2b7d5438250706f23a825f3628"
|
||||
content-hash = "4544b2a0b0065aa9e13d9a3b5a951fb5212921fe72f0fe259069e2e9205e9830"
|
||||
|
||||
@@ -108,6 +108,7 @@ drf-spectacular = "*"
|
||||
dumb-init = "*"
|
||||
duo-client = "*"
|
||||
facebook-sdk = "*"
|
||||
fido2 = "*"
|
||||
flower = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
|
||||
+317
-144
@@ -18405,7 +18405,7 @@ paths:
|
||||
- authentik_stages_authenticator_totp.authenticatortotpstage
|
||||
- authentik_stages_authenticator_totp.totpdevice
|
||||
- authentik_stages_authenticator_validate.authenticatorvalidatestage
|
||||
- authentik_stages_authenticator_webauthn.authenticatewebauthnstage
|
||||
- authentik_stages_authenticator_webauthn.authenticatorwebauthnstage
|
||||
- authentik_stages_authenticator_webauthn.webauthndevice
|
||||
- authentik_stages_captcha.captchastage
|
||||
- authentik_stages_consent.consentstage
|
||||
@@ -18619,7 +18619,7 @@ paths:
|
||||
- authentik_stages_authenticator_totp.authenticatortotpstage
|
||||
- authentik_stages_authenticator_totp.totpdevice
|
||||
- authentik_stages_authenticator_validate.authenticatorvalidatestage
|
||||
- authentik_stages_authenticator_webauthn.authenticatewebauthnstage
|
||||
- authentik_stages_authenticator_webauthn.authenticatorwebauthnstage
|
||||
- authentik_stages_authenticator_webauthn.webauthndevice
|
||||
- authentik_stages_captcha.captchastage
|
||||
- authentik_stages_consent.consentstage
|
||||
@@ -24007,7 +24007,7 @@ paths:
|
||||
/stages/authenticator/webauthn/:
|
||||
get:
|
||||
operationId: stages_authenticator_webauthn_list
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
description: AuthenticatorWebAuthnStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: authenticator_attachment
|
||||
@@ -24022,6 +24022,15 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: device_type_restrictions
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: friendly_name
|
||||
schema:
|
||||
@@ -24084,7 +24093,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedAuthenticateWebAuthnStageList'
|
||||
$ref: '#/components/schemas/PaginatedAuthenticatorWebAuthnStageList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
@@ -24100,14 +24109,14 @@ paths:
|
||||
description: ''
|
||||
post:
|
||||
operationId: stages_authenticator_webauthn_create
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
description: AuthenticatorWebAuthnStage Viewset
|
||||
tags:
|
||||
- stages
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStageRequest'
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStageRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
@@ -24116,7 +24125,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStage'
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
@@ -24133,7 +24142,7 @@ paths:
|
||||
/stages/authenticator/webauthn/{stage_uuid}/:
|
||||
get:
|
||||
operationId: stages_authenticator_webauthn_retrieve
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
description: AuthenticatorWebAuthnStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
@@ -24151,7 +24160,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStage'
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
@@ -24167,7 +24176,7 @@ paths:
|
||||
description: ''
|
||||
put:
|
||||
operationId: stages_authenticator_webauthn_update
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
description: AuthenticatorWebAuthnStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
@@ -24182,7 +24191,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStageRequest'
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStageRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
@@ -24191,7 +24200,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStage'
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
@@ -24207,7 +24216,7 @@ paths:
|
||||
description: ''
|
||||
patch:
|
||||
operationId: stages_authenticator_webauthn_partial_update
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
description: AuthenticatorWebAuthnStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
@@ -24222,7 +24231,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedAuthenticateWebAuthnStageRequest'
|
||||
$ref: '#/components/schemas/PatchedAuthenticatorWebAuthnStageRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
@@ -24230,7 +24239,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStage'
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStage'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
@@ -24246,7 +24255,7 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: stages_authenticator_webauthn_destroy
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
description: AuthenticatorWebAuthnStage Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: stage_uuid
|
||||
@@ -24311,6 +24320,106 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/stages/authenticator/webauthn_device_types/:
|
||||
get:
|
||||
operationId: stages_authenticator_webauthn_device_types_list
|
||||
description: WebAuthnDeviceType Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: aaguid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: description
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: icon
|
||||
schema:
|
||||
type: string
|
||||
- name: ordering
|
||||
required: false
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
required: false
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
schema:
|
||||
type: integer
|
||||
- name: page_size
|
||||
required: false
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedWebAuthnDeviceTypeList'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/stages/authenticator/webauthn_device_types/{aaguid}/:
|
||||
get:
|
||||
operationId: stages_authenticator_webauthn_device_types_retrieve
|
||||
description: WebAuthnDeviceType Viewset
|
||||
parameters:
|
||||
- in: path
|
||||
name: aaguid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this WebAuthn Device type.
|
||||
required: true
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WebAuthnDeviceType'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/stages/captcha/:
|
||||
get:
|
||||
operationId: stages_captcha_list
|
||||
@@ -29738,92 +29847,6 @@ components:
|
||||
- basic
|
||||
- bearer
|
||||
type: string
|
||||
AuthenticateWebAuthnStage:
|
||||
type: object
|
||||
description: AuthenticateWebAuthnStage Serializer
|
||||
properties:
|
||||
pk:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
title: Stage uuid
|
||||
name:
|
||||
type: string
|
||||
component:
|
||||
type: string
|
||||
description: Get object type so that we know how to edit the object
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
type: string
|
||||
description: Return object's plural verbose_name
|
||||
readOnly: true
|
||||
meta_model_name:
|
||||
type: string
|
||||
description: Return internal model name
|
||||
readOnly: true
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSet'
|
||||
configure_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used by an authenticated user to configure this Stage.
|
||||
If empty, user will not be able to configure this stage.
|
||||
friendly_name:
|
||||
type: string
|
||||
nullable: true
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
required:
|
||||
- component
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
AuthenticateWebAuthnStageRequest:
|
||||
type: object
|
||||
description: AuthenticateWebAuthnStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
configure_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used by an authenticated user to configure this Stage.
|
||||
If empty, user will not be able to configure this stage.
|
||||
friendly_name:
|
||||
type: string
|
||||
nullable: true
|
||||
minLength: 1
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
required:
|
||||
- name
|
||||
AuthenticatedSession:
|
||||
type: object
|
||||
description: AuthenticatedSession Serializer
|
||||
@@ -30738,6 +30761,108 @@ components:
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- response
|
||||
AuthenticatorWebAuthnStage:
|
||||
type: object
|
||||
description: AuthenticatorWebAuthnStage Serializer
|
||||
properties:
|
||||
pk:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
title: Stage uuid
|
||||
name:
|
||||
type: string
|
||||
component:
|
||||
type: string
|
||||
description: Get object type so that we know how to edit the object
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
type: string
|
||||
description: Return object's verbose_name
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
type: string
|
||||
description: Return object's plural verbose_name
|
||||
readOnly: true
|
||||
meta_model_name:
|
||||
type: string
|
||||
description: Return internal model name
|
||||
readOnly: true
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSet'
|
||||
configure_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used by an authenticated user to configure this Stage.
|
||||
If empty, user will not be able to configure this stage.
|
||||
friendly_name:
|
||||
type: string
|
||||
nullable: true
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
device_type_restrictions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
device_type_restrictions_obj:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WebAuthnDeviceType'
|
||||
readOnly: true
|
||||
required:
|
||||
- component
|
||||
- device_type_restrictions_obj
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
AuthenticatorWebAuthnStageRequest:
|
||||
type: object
|
||||
description: AuthenticatorWebAuthnStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
configure_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used by an authenticated user to configure this Stage.
|
||||
If empty, user will not be able to configure this stage.
|
||||
friendly_name:
|
||||
type: string
|
||||
nullable: true
|
||||
minLength: 1
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
device_type_restrictions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- name
|
||||
AutoSubmitChallengeResponseRequest:
|
||||
type: object
|
||||
description: Pseudo class for autosubmit response
|
||||
@@ -34706,7 +34831,7 @@ components:
|
||||
- authentik_stages_authenticator_totp.authenticatortotpstage
|
||||
- authentik_stages_authenticator_totp.totpdevice
|
||||
- authentik_stages_authenticator_validate.authenticatorvalidatestage
|
||||
- authentik_stages_authenticator_webauthn.authenticatewebauthnstage
|
||||
- authentik_stages_authenticator_webauthn.authenticatorwebauthnstage
|
||||
- authentik_stages_authenticator_webauthn.webauthndevice
|
||||
- authentik_stages_captcha.captchastage
|
||||
- authentik_stages_consent.consentstage
|
||||
@@ -35673,18 +35798,6 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedAuthenticateWebAuthnStageList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthenticateWebAuthnStage'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedAuthenticatedSessionList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -35757,6 +35870,18 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedAuthenticatorWebAuthnStageList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthenticatorWebAuthnStage'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedBlueprintInstanceList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -36825,6 +36950,18 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedWebAuthnDeviceTypeList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WebAuthnDeviceType'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
Pagination:
|
||||
type: object
|
||||
properties:
|
||||
@@ -37230,35 +37367,6 @@ components:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
group:
|
||||
type: string
|
||||
PatchedAuthenticateWebAuthnStageRequest:
|
||||
type: object
|
||||
description: AuthenticateWebAuthnStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
configure_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used by an authenticated user to configure this Stage.
|
||||
If empty, user will not be able to configure this stage.
|
||||
friendly_name:
|
||||
type: string
|
||||
nullable: true
|
||||
minLength: 1
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
PatchedAuthenticatorDuoStageRequest:
|
||||
type: object
|
||||
description: AuthenticatorDuoStage Serializer
|
||||
@@ -37428,6 +37536,40 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserVerificationEnum'
|
||||
description: Enforce user verification for WebAuthn devices.
|
||||
PatchedAuthenticatorWebAuthnStageRequest:
|
||||
type: object
|
||||
description: AuthenticatorWebAuthnStage Serializer
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
flow_set:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowSetRequest'
|
||||
configure_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Flow used by an authenticated user to configure this Stage.
|
||||
If empty, user will not be able to configure this stage.
|
||||
friendly_name:
|
||||
type: string
|
||||
nullable: true
|
||||
minLength: 1
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
device_type_restrictions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
PatchedBlueprintInstanceRequest:
|
||||
type: object
|
||||
description: Info about a single blueprint instance file
|
||||
@@ -44133,8 +44275,14 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
device_type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/WebAuthnDeviceType'
|
||||
readOnly: true
|
||||
nullable: true
|
||||
required:
|
||||
- created_on
|
||||
- device_type
|
||||
- name
|
||||
- pk
|
||||
WebAuthnDeviceRequest:
|
||||
@@ -44147,6 +44295,31 @@ components:
|
||||
maxLength: 200
|
||||
required:
|
||||
- name
|
||||
WebAuthnDeviceType:
|
||||
type: object
|
||||
description: WebAuthnDeviceType Serializer
|
||||
properties:
|
||||
aaguid:
|
||||
type: string
|
||||
format: uuid
|
||||
description:
|
||||
type: string
|
||||
required:
|
||||
- aaguid
|
||||
- description
|
||||
WebAuthnDeviceTypeRequest:
|
||||
type: object
|
||||
description: WebAuthnDeviceType Serializer
|
||||
properties:
|
||||
aaguid:
|
||||
type: string
|
||||
format: uuid
|
||||
description:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- aaguid
|
||||
- description
|
||||
Workers:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -5,7 +5,7 @@ import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_validate/AuthenticatorValidateStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_webauthn/AuthenticatorWebAuthnStageForm";
|
||||
import "@goauthentik/admin/stages/captcha/CaptchaStageForm";
|
||||
import "@goauthentik/admin/stages/consent/ConsentStageForm";
|
||||
import "@goauthentik/admin/stages/deny/DenyStageForm";
|
||||
|
||||
@@ -5,7 +5,7 @@ import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_validate/AuthenticatorValidateStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm";
|
||||
import "@goauthentik/admin/stages/authenticator_webauthn/AuthenticatorWebAuthnStageForm";
|
||||
import "@goauthentik/admin/stages/captcha/CaptchaStageForm";
|
||||
import "@goauthentik/admin/stages/consent/ConsentStageForm";
|
||||
import "@goauthentik/admin/stages/deny/DenyStageForm";
|
||||
|
||||
+54
-6
@@ -1,7 +1,12 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import {
|
||||
DataProvision,
|
||||
DualSelectPair,
|
||||
} from "@goauthentik/authentik/elements/ak-dual-select/types";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
@@ -11,8 +16,8 @@ import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
AuthenticateWebAuthnStage,
|
||||
AuthenticatorAttachmentEnum,
|
||||
AuthenticatorWebAuthnStage,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
@@ -20,28 +25,39 @@ import {
|
||||
ResidentKeyRequirementEnum,
|
||||
StagesApi,
|
||||
UserVerificationEnum,
|
||||
WebAuthnDeviceType,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-authenticator-webauthn-form")
|
||||
export class AuthenticateWebAuthnStageForm extends BaseStageForm<AuthenticateWebAuthnStage> {
|
||||
loadInstance(pk: string): Promise<AuthenticateWebAuthnStage> {
|
||||
export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorWebAuthnStage> {
|
||||
deviceTypeRestrictionPair(item: WebAuthnDeviceType): DualSelectPair {
|
||||
const label = item.description ? item.description : item.aaguid;
|
||||
return [
|
||||
item.aaguid,
|
||||
html`<div class="selection-main">${label}</div>
|
||||
<div class="selection-desc">${item.aaguid}</div>`,
|
||||
label,
|
||||
];
|
||||
}
|
||||
|
||||
loadInstance(pk: string): Promise<AuthenticatorWebAuthnStage> {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRetrieve({
|
||||
stageUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: AuthenticateWebAuthnStage): Promise<AuthenticateWebAuthnStage> {
|
||||
async send(data: AuthenticatorWebAuthnStage): Promise<AuthenticatorWebAuthnStage> {
|
||||
if (data.authenticatorAttachment?.toString() === "") {
|
||||
data.authenticatorAttachment = null;
|
||||
}
|
||||
if (this.instance) {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnUpdate({
|
||||
stageUuid: this.instance.pk || "",
|
||||
authenticateWebAuthnStageRequest: data,
|
||||
authenticatorWebAuthnStageRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnCreate({
|
||||
authenticateWebAuthnStageRequest: data,
|
||||
authenticatorWebAuthnStageRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -164,6 +180,38 @@ export class AuthenticateWebAuthnStageForm extends BaseStageForm<AuthenticateWeb
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Device type restrictions")}
|
||||
name="deviceTypeRestrictions"
|
||||
>
|
||||
<ak-dual-select-provider
|
||||
.provider=${(page: number, search?: string): Promise<DataProvision> => {
|
||||
return new StagesApi(DEFAULT_CONFIG)
|
||||
.stagesAuthenticatorWebauthnDeviceTypesList({
|
||||
page: page,
|
||||
search: search,
|
||||
})
|
||||
.then((results) => {
|
||||
return {
|
||||
pagination: results.pagination,
|
||||
options: results.results.map(
|
||||
this.deviceTypeRestrictionPair,
|
||||
),
|
||||
};
|
||||
});
|
||||
}}
|
||||
.selected=${(this.instance?.deviceTypeRestrictionsObj ?? []).map(
|
||||
this.deviceTypeRestrictionPair,
|
||||
)}
|
||||
available-label="${msg("Available Device types")}"
|
||||
selected-label="${msg("Selected Device types")}"
|
||||
></ak-dual-select-provider>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Configuration flow")}
|
||||
name="configureFlow"
|
||||
@@ -10,6 +10,7 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -130,6 +131,17 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-empty-state
|
||||
?loading="${this.registerRunning}"
|
||||
header=${this.registerRunning
|
||||
|
||||
@@ -4,4 +4,26 @@ title: WebAuthn authenticator setup stage
|
||||
|
||||
This stage configures a WebAuthn-based Authenticator. This can either be a browser, biometrics or a Security stick like a YubiKey.
|
||||
|
||||
There are no stage-specific settings.
|
||||
### `User verification`
|
||||
|
||||
Configure if authentik should require, prefer or discourage user verification for the authenticator. For example when using a virtual authenticator like Windows Hello, this setting controls if a PIN is required.
|
||||
|
||||
### `Resident key requirement`
|
||||
|
||||
Configure if the created authenticator is stored in the encrypted memory on the device or in persistent memory. When configuring [passwordless login](../identification/index.md#passwordless-flow), this should be set to either _Preferred_ or _Required_, otherwise the authenticator cannot be used for passwordless authentication.
|
||||
|
||||
### `Authenticator Attachment`
|
||||
|
||||
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.
|
||||
|
||||
### `Device type restrictions`
|
||||
|
||||
:::info
|
||||
Requires authentik 2024.4
|
||||
:::
|
||||
|
||||
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.
|
||||
|
||||
When no restrictions are selected, all device types are allowed.
|
||||
|
||||
As authentik does not know of all possible device types, it is possible to select the special option `authentik: Unknown devices` to allow unknown devices.
|
||||
|
||||
Reference in New Issue
Block a user