diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index 88a0569b42f9..f0cc804f521a 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -51,6 +51,10 @@ MicrosoftEntraProviderUser, ) from authentik.enterprise.providers.rac.models import ConnectionToken +from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( + EndpointDevice, + EndpointDeviceConnection, +) from authentik.events.logs import LogEvent, capture_logs from authentik.events.models import SystemTask from authentik.events.utils import cleanse_dict @@ -119,6 +123,8 @@ def excluded_models() -> list[type[Model]]: GoogleWorkspaceProviderGroup, MicrosoftEntraProviderUser, MicrosoftEntraProviderGroup, + EndpointDevice, + EndpointDeviceConnection, ) diff --git a/authentik/core/api/devices.py b/authentik/core/api/devices.py index 94484505d5a5..58040df8358f 100644 --- a/authentik/core/api/devices.py +++ b/authentik/core/api/devices.py @@ -6,7 +6,6 @@ BooleanField, CharField, DateTimeField, - IntegerField, SerializerMethodField, ) from rest_framework.permissions import IsAuthenticated @@ -15,6 +14,7 @@ from rest_framework.viewsets import ViewSet from authentik.core.api.utils import MetaNameSerializer +from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice from authentik.rbac.decorators import permission_required from authentik.stages.authenticator import device_classes, devices_for_user from authentik.stages.authenticator.models import Device @@ -24,7 +24,7 @@ class DeviceSerializer(MetaNameSerializer): """Serializer for Duo authenticator devices""" - pk = IntegerField() + pk = CharField() name = CharField() type = SerializerMethodField() confirmed = BooleanField() @@ -41,6 +41,8 @@ def get_extra_description(self, instance: Device) -> str: """Get extra description""" if isinstance(instance, WebAuthnDevice): return instance.device_type.description + if isinstance(instance, EndpointDevice): + return instance.data.get("deviceSignals", {}).get("deviceModel") return "" diff --git a/authentik/core/management/commands/shell.py b/authentik/core/management/commands/shell.py index 6731069ae311..3d3188001998 100644 --- a/authentik/core/management/commands/shell.py +++ b/authentik/core/management/commands/shell.py @@ -4,6 +4,7 @@ import platform import sys import traceback +from pprint import pprint from django.apps import apps from django.core.management.base import BaseCommand @@ -34,7 +35,9 @@ def add_arguments(self, parser): def get_namespace(self): """Prepare namespace with all models""" - namespace = {} + namespace = { + "pprint": pprint, + } # Gather Django models and constants from each app for app in apps.get_app_configs(): diff --git a/authentik/core/tests/test_devices_api.py b/authentik/core/tests/test_devices_api.py index 0f3d584506ed..4bac6cd8ca90 100644 --- a/authentik/core/tests/test_devices_api.py +++ b/authentik/core/tests/test_devices_api.py @@ -29,7 +29,7 @@ def test_user_api(self): self.assertEqual(response.status_code, 200) body = loads(response.content.decode()) self.assertEqual(len(body), 1) - self.assertEqual(body[0]["pk"], self.device1.pk) + self.assertEqual(body[0]["pk"], str(self.device1.pk)) def test_user_api_as_admin(self): """Test user API""" @@ -54,4 +54,6 @@ def test_admin_api(self): self.assertEqual(response.status_code, 200) body = loads(response.content.decode()) self.assertEqual(len(body), 2) - self.assertEqual({body[0]["pk"], body[1]["pk"]}, {self.device1.pk, self.device2.pk}) + self.assertEqual( + {body[0]["pk"], body[1]["pk"]}, {str(self.device1.pk), str(self.device2.pk)} + ) diff --git a/authentik/enterprise/settings.py b/authentik/enterprise/settings.py index 0a35d52d3572..318493ef6c5b 100644 --- a/authentik/enterprise/settings.py +++ b/authentik/enterprise/settings.py @@ -17,6 +17,7 @@ "authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.microsoft_entra", "authentik.enterprise.providers.rac", + "authentik.enterprise.stages.authenticator_endpoint_gdtc", "authentik.enterprise.stages.source", ] diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/api.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/api.py new file mode 100644 index 000000000000..598f2403a4e5 --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/api.py @@ -0,0 +1,82 @@ +"""AuthenticatorEndpointGDTCStage API Views""" + +from django_filters.rest_framework.backends import DjangoFilterBackend +from rest_framework import mixins +from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.permissions import IsAdminUser +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import GenericViewSet, ModelViewSet +from structlog.stdlib import get_logger + +from authentik.api.authorization import OwnerFilter, OwnerPermissions +from authentik.core.api.used_by import UsedByMixin +from authentik.enterprise.api import EnterpriseRequiredMixin +from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( + AuthenticatorEndpointGDTCStage, + EndpointDevice, +) +from authentik.flows.api.stages import StageSerializer + +LOGGER = get_logger() + + +class AuthenticatorEndpointGDTCStageSerializer(EnterpriseRequiredMixin, StageSerializer): + """AuthenticatorEndpointGDTCStage Serializer""" + + class Meta: + model = AuthenticatorEndpointGDTCStage + fields = StageSerializer.Meta.fields + [ + "configure_flow", + "friendly_name", + "credentials", + ] + + +class AuthenticatorEndpointGDTCStageViewSet(UsedByMixin, ModelViewSet): + """AuthenticatorEndpointGDTCStage Viewset""" + + queryset = AuthenticatorEndpointGDTCStage.objects.all() + serializer_class = AuthenticatorEndpointGDTCStageSerializer + filterset_fields = [ + "name", + "configure_flow", + ] + search_fields = ["name"] + ordering = ["name"] + + +class EndpointDeviceSerializer(ModelSerializer): + """Serializer for Endpoint authenticator devices""" + + class Meta: + model = EndpointDevice + fields = ["pk", "name"] + depth = 2 + + +class EndpointDeviceViewSet( + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + UsedByMixin, + GenericViewSet, +): + """Viewset for Endpoint authenticator devices""" + + queryset = EndpointDevice.objects.all() + serializer_class = EndpointDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] + + +class EndpointAdminDeviceViewSet(ModelViewSet): + """Viewset for Endpoint authenticator devices (for admins)""" + + permission_classes = [IsAdminUser] + queryset = EndpointDevice.objects.all() + serializer_class = EndpointDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/apps.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/apps.py new file mode 100644 index 000000000000..71eae3d42b9d --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/apps.py @@ -0,0 +1,13 @@ +"""authentik Endpoint app config""" + +from authentik.enterprise.apps import EnterpriseConfig + + +class AuthentikStageAuthenticatorEndpointConfig(EnterpriseConfig): + """authentik endpoint config""" + + name = "authentik.enterprise.stages.authenticator_endpoint_gdtc" + label = "authentik_stages_authenticator_endpoint_gdtc" + verbose_name = "authentik Enterprise.Stages.Authenticator.Endpoint GDTC" + default = True + mountpoint = "endpoint/gdtc/" diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/migrations/0001_initial.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/migrations/0001_initial.py new file mode 100644 index 000000000000..a498c732092f --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/migrations/0001_initial.py @@ -0,0 +1,115 @@ +# Generated by Django 5.0.9 on 2024-10-22 11:40 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("authentik_flows", "0027_auto_20231028_1424"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="AuthenticatorEndpointGDTCStage", + fields=[ + ( + "stage_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_flows.stage", + ), + ), + ("friendly_name", models.TextField(null=True)), + ("credentials", models.JSONField()), + ( + "configure_flow", + models.ForeignKey( + blank=True, + help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="authentik_flows.flow", + ), + ), + ], + options={ + "verbose_name": "Endpoint Authenticator Google Device Trust Connector Stage", + "verbose_name_plural": "Endpoint Authenticator Google Device Trust Connector Stages", + }, + bases=("authentik_flows.stage", models.Model), + ), + migrations.CreateModel( + name="EndpointDevice", + fields=[ + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "name", + models.CharField( + help_text="The human-readable name of this device.", max_length=64 + ), + ), + ( + "confirmed", + models.BooleanField(default=True, help_text="Is this device ready for use?"), + ), + ("last_used", models.DateTimeField(null=True)), + ("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ( + "host_identifier", + models.TextField( + help_text="A unique identifier for the endpoint device, usually the device serial number", + unique=True, + ), + ), + ("data", models.JSONField()), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "verbose_name": "Endpoint Device", + "verbose_name_plural": "Endpoint Devices", + }, + ), + migrations.CreateModel( + name="EndpointDeviceConnection", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("attributes", models.JSONField()), + ( + "device", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_stages_authenticator_endpoint_gdtc.endpointdevice", + ), + ), + ( + "stage", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage", + ), + ), + ], + ), + ] diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/migrations/__init__.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py new file mode 100644 index 000000000000..8854d9bf70fc --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py @@ -0,0 +1,101 @@ +"""Endpoint stage""" + +from uuid import uuid4 + +from django.contrib.auth import get_user_model +from django.db import models +from django.utils.translation import gettext_lazy as _ +from google.oauth2.service_account import Credentials +from rest_framework.serializers import BaseSerializer, Serializer + +from authentik.core.types import UserSettingSerializer +from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage +from authentik.flows.stage import StageView +from authentik.lib.models import SerializerModel +from authentik.stages.authenticator.models import Device + + +class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage): + """Setup Google Chrome Device-trust connection""" + + credentials = models.JSONField() + + def google_credentials(self): + return { + "credentials": Credentials.from_service_account_info( + self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"] + ), + } + + @property + def serializer(self) -> type[BaseSerializer]: + from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import ( + AuthenticatorEndpointGDTCStageSerializer, + ) + + return AuthenticatorEndpointGDTCStageSerializer + + @property + def view(self) -> type[StageView]: + from authentik.enterprise.stages.authenticator_endpoint_gdtc.stage import ( + AuthenticatorEndpointStageView, + ) + + return AuthenticatorEndpointStageView + + @property + def component(self) -> str: + return "ak-stage-authenticator-endpoint-gdtc-form" + + def ui_user_settings(self) -> UserSettingSerializer | None: + return UserSettingSerializer( + data={ + "title": self.friendly_name or str(self._meta.verbose_name), + "component": "ak-user-settings-authenticator-endpoint", + } + ) + + def __str__(self) -> str: + return f"Endpoint Authenticator Google Device Trust Connector Stage {self.name}" + + class Meta: + verbose_name = _("Endpoint Authenticator Google Device Trust Connector Stage") + verbose_name_plural = _("Endpoint Authenticator Google Device Trust Connector Stages") + + +class EndpointDevice(SerializerModel, Device): + """Endpoint Device for a single user""" + + uuid = models.UUIDField(primary_key=True, default=uuid4) + host_identifier = models.TextField( + unique=True, + help_text="A unique identifier for the endpoint device, usually the device serial number", + ) + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + data = models.JSONField() + + @property + def serializer(self) -> Serializer: + from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import ( + EndpointDeviceSerializer, + ) + + return EndpointDeviceSerializer + + def __str__(self): + return str(self.name) or str(self.user_id) + + class Meta: + verbose_name = _("Endpoint Device") + verbose_name_plural = _("Endpoint Devices") + + +class EndpointDeviceConnection(models.Model): + device = models.ForeignKey(EndpointDevice, on_delete=models.CASCADE) + stage = models.ForeignKey(AuthenticatorEndpointGDTCStage, on_delete=models.CASCADE) + + attributes = models.JSONField() + + def __str__(self) -> str: + return f"Endpoint device connection {self.device_id} to {self.stage_id}" diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py new file mode 100644 index 000000000000..2ca629c6247e --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py @@ -0,0 +1,32 @@ +from django.http import HttpResponse +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from authentik.flows.challenge import ( + Challenge, + ChallengeResponse, + FrameChallenge, + FrameChallengeResponse, +) +from authentik.flows.stage import ChallengeStageView + + +class AuthenticatorEndpointStageView(ChallengeStageView): + """Endpoint stage""" + + response_class = FrameChallengeResponse + + def get_challenge(self, *args, **kwargs) -> Challenge: + return FrameChallenge( + data={ + "component": "xak-flow-frame", + "url": self.request.build_absolute_uri( + reverse("authentik_stages_authenticator_endpoint_gdtc:chrome") + ), + "loading_overlay": True, + "loading_text": _("Verifying your browser..."), + } + ) + + def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: + return self.executor.stage_ok() diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/templates/stages/authenticator_endpoint/google_chrome_dtc.html b/authentik/enterprise/stages/authenticator_endpoint_gdtc/templates/stages/authenticator_endpoint/google_chrome_dtc.html new file mode 100644 index 000000000000..d2f2aefc4647 --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/templates/stages/authenticator_endpoint/google_chrome_dtc.html @@ -0,0 +1,9 @@ + + + diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/urls.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/urls.py new file mode 100644 index 000000000000..d274909e9427 --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/urls.py @@ -0,0 +1,26 @@ +"""API URLs""" + +from django.urls import path + +from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import ( + AuthenticatorEndpointGDTCStageViewSet, + EndpointAdminDeviceViewSet, + EndpointDeviceViewSet, +) +from authentik.enterprise.stages.authenticator_endpoint_gdtc.views.dtc import ( + GoogleChromeDeviceTrustConnector, +) + +urlpatterns = [ + path("chrome/", GoogleChromeDeviceTrustConnector.as_view(), name="chrome"), +] + +api_urlpatterns = [ + ("authenticators/endpoint", EndpointDeviceViewSet), + ( + "authenticators/admin/endpoint", + EndpointAdminDeviceViewSet, + "admin-endpointdevice", + ), + ("stages/authenticator/endpoint_gdtc", AuthenticatorEndpointGDTCStageViewSet), +] diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/views/__init__.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/views/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/stages/authenticator_endpoint_gdtc/views/dtc.py b/authentik/enterprise/stages/authenticator_endpoint_gdtc/views/dtc.py new file mode 100644 index 000000000000..e81865714939 --- /dev/null +++ b/authentik/enterprise/stages/authenticator_endpoint_gdtc/views/dtc.py @@ -0,0 +1,84 @@ +from json import dumps, loads +from typing import Any + +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.template.response import TemplateResponse +from django.urls import reverse +from django.views import View +from googleapiclient.discovery import build + +from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( + AuthenticatorEndpointGDTCStage, + EndpointDevice, + EndpointDeviceConnection, +) +from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.views.executor import SESSION_KEY_PLAN +from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS + +# Header we get from chrome that initiates verified access +HEADER_DEVICE_TRUST = "X-Device-Trust" +# Header we send to the client with the challenge +HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge" +# Header we get back from the client that we verify with google +HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response" +# Header value for x-device-trust that initiates the flow +DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess" + + +class GoogleChromeDeviceTrustConnector(View): + """Google Chrome Device-trust connector based endpoint authenticator""" + + def get_flow_plan(self) -> FlowPlan: + flow_plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] + return flow_plan + + def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: + super().setup(request, *args, **kwargs) + stage: AuthenticatorEndpointGDTCStage = self.get_flow_plan().bindings[0].stage + self.google_client = build( + "verifiedaccess", + "v2", + cache_discovery=False, + **stage.google_credentials(), + ) + + def get(self, request: HttpRequest) -> HttpResponse: + x_device_trust = request.headers.get(HEADER_DEVICE_TRUST) + x_access_challenge_response = request.headers.get(HEADER_ACCESS_CHALLENGE_RESPONSE) + if x_device_trust == "VerifiedAccess" and x_access_challenge_response is None: + challenge = self.google_client.challenge().generate().execute() + res = HttpResponseRedirect( + self.request.build_absolute_uri( + reverse("authentik_stages_authenticator_endpoint_gdtc:chrome") + ) + ) + res[HEADER_ACCESS_CHALLENGE] = dumps(challenge) + return res + if x_access_challenge_response: + response = ( + self.google_client.challenge() + .verify(body=loads(x_access_challenge_response)) + .execute() + ) + # Remove deprecated string representation of deviceSignals + response.pop("deviceSignal", None) + flow_plan: FlowPlan = self.get_flow_plan() + device, _ = EndpointDevice.objects.update_or_create( + host_identifier=response["deviceSignals"]["serialNumber"], + user=flow_plan.context.get(PLAN_CONTEXT_PENDING_USER), + defaults={"name": response["deviceSignals"]["hostname"], "data": response}, + ) + EndpointDeviceConnection.objects.update_or_create( + device=device, + stage=flow_plan.bindings[0].stage, + defaults={ + "attributes": response, + }, + ) + flow_plan.context.setdefault(PLAN_CONTEXT_METHOD, "trusted_endpoint") + flow_plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {}) + flow_plan.context[PLAN_CONTEXT_METHOD_ARGS].setdefault("endpoints", []) + flow_plan.context[PLAN_CONTEXT_METHOD_ARGS]["endpoints"].append(response) + request.session[SESSION_KEY_PLAN] = flow_plan + return TemplateResponse(request, "stages/authenticator_endpoint/google_chrome_dtc.html") diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index deb3b3483b4b..dfb3585ec4c4 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -8,7 +8,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.http import JsonResponse -from rest_framework.fields import CharField, ChoiceField, DictField +from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField from rest_framework.request import Request from authentik.core.api.utils import PassiveSerializer @@ -160,6 +160,20 @@ class AutoSubmitChallengeResponse(ChallengeResponse): component = CharField(default="ak-stage-autosubmit") +class FrameChallenge(Challenge): + """Challenge type to render a frame""" + + component = CharField(default="xak-flow-frame") + url = CharField() + loading_overlay = BooleanField(default=False) + loading_text = CharField() + + +class FrameChallengeResponse(ChallengeResponse): + + component = CharField(default="xak-flow-frame") + + class DataclassEncoder(DjangoJSONEncoder): """Convert any dataclass to json""" diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 8b3c7666b3cf..3b69f89d5287 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -38,6 +38,7 @@ SESSION_COOKIE_NAME = "authentik_session" SESSION_COOKIE_DOMAIN = CONFIG.get("cookie_domain", None) APPEND_SLASH = False +X_FRAME_OPTIONS = "SAMEORIGIN" AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py index fc0e7f686734..b8781bb835d4 100644 --- a/authentik/stages/authenticator_webauthn/models.py +++ b/authentik/stages/authenticator_webauthn/models.py @@ -68,7 +68,7 @@ class AuthenticatorAttachment(models.TextChoices): class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage): - """WebAuthn stage""" + """Stage to enroll WebAuthn-based authenticators.""" user_verification = models.TextField( choices=UserVerification.choices, diff --git a/blueprints/schema.json b/blueprints/schema.json index 802ce9b268c0..033c6b324624 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -3361,6 +3361,46 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "present", + "created", + "must_created" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/$defs/model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage" + } + } + }, { "type": "object", "required": [ @@ -4304,6 +4344,7 @@ "authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.microsoft_entra", "authentik.enterprise.providers.rac", + "authentik.enterprise.stages.authenticator_endpoint_gdtc", "authentik.enterprise.stages.source", "authentik.events" ], @@ -4400,6 +4441,7 @@ "authentik_providers_rac.racprovider", "authentik_providers_rac.endpoint", "authentik_providers_rac.racpropertymapping", + "authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage", "authentik_stages_source.sourcestage", "authentik_events.event", "authentik_events.notificationtransport", @@ -6451,6 +6493,18 @@ "authentik_stages_authenticator_duo.delete_duodevice", "authentik_stages_authenticator_duo.view_authenticatorduostage", "authentik_stages_authenticator_duo.view_duodevice", + "authentik_stages_authenticator_endpoint_gdtc.add_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.add_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.add_endpointdeviceconnection", + "authentik_stages_authenticator_endpoint_gdtc.change_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.change_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.change_endpointdeviceconnection", + "authentik_stages_authenticator_endpoint_gdtc.delete_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.delete_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.delete_endpointdeviceconnection", + "authentik_stages_authenticator_endpoint_gdtc.view_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.view_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.view_endpointdeviceconnection", "authentik_stages_authenticator_sms.add_authenticatorsmsstage", "authentik_stages_authenticator_sms.add_smsdevice", "authentik_stages_authenticator_sms.change_authenticatorsmsstage", @@ -12107,6 +12161,18 @@ "authentik_stages_authenticator_duo.delete_duodevice", "authentik_stages_authenticator_duo.view_authenticatorduostage", "authentik_stages_authenticator_duo.view_duodevice", + "authentik_stages_authenticator_endpoint_gdtc.add_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.add_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.add_endpointdeviceconnection", + "authentik_stages_authenticator_endpoint_gdtc.change_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.change_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.change_endpointdeviceconnection", + "authentik_stages_authenticator_endpoint_gdtc.delete_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.delete_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.delete_endpointdeviceconnection", + "authentik_stages_authenticator_endpoint_gdtc.view_authenticatorendpointgdtcstage", + "authentik_stages_authenticator_endpoint_gdtc.view_endpointdevice", + "authentik_stages_authenticator_endpoint_gdtc.view_endpointdeviceconnection", "authentik_stages_authenticator_sms.add_authenticatorsmsstage", "authentik_stages_authenticator_sms.add_smsdevice", "authentik_stages_authenticator_sms.change_authenticatorsmsstage", @@ -12997,6 +13063,144 @@ } } }, + "model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name" + }, + "flow_set": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name" + }, + "slug": { + "type": "string", + "maxLength": 50, + "minLength": 1, + "pattern": "^[-a-zA-Z0-9_]+$", + "title": "Slug", + "description": "Visible in the URL." + }, + "title": { + "type": "string", + "minLength": 1, + "title": "Title", + "description": "Shown as the Title in Flow pages." + }, + "designation": { + "type": "string", + "enum": [ + "authentication", + "authorization", + "invalidation", + "enrollment", + "unenrollment", + "recovery", + "stage_configuration" + ], + "title": "Designation", + "description": "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." + }, + "policy_engine_mode": { + "type": "string", + "enum": [ + "all", + "any" + ], + "title": "Policy engine mode" + }, + "compatibility_mode": { + "type": "boolean", + "title": "Compatibility mode", + "description": "Enable compatibility mode, increases compatibility with password managers on mobile devices." + }, + "layout": { + "type": "string", + "enum": [ + "stacked", + "content_left", + "content_right", + "sidebar_left", + "sidebar_right" + ], + "title": "Layout" + }, + "denied_action": { + "type": "string", + "enum": [ + "message_continue", + "message", + "continue" + ], + "title": "Denied action", + "description": "Configure what should happen when a flow denies access to a user." + } + }, + "required": [ + "name", + "slug", + "title", + "designation" + ] + }, + "title": "Flow set" + }, + "configure_flow": { + "type": "string", + "format": "uuid", + "title": "Configure flow", + "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", + "null" + ], + "minLength": 1, + "title": "Friendly name" + }, + "credentials": { + "type": "object", + "additionalProperties": true, + "title": "Credentials" + } + }, + "required": [] + }, + "model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_authenticatorendpointgdtcstage", + "change_authenticatorendpointgdtcstage", + "delete_authenticatorendpointgdtcstage", + "view_authenticatorendpointgdtcstage" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, "model_authentik_stages_source.sourcestage": { "type": "object", "properties": { diff --git a/schema.yml b/schema.yml index f8b5472862b7..23bb4140bcb7 100644 --- a/schema.yml +++ b/schema.yml @@ -636,6 +636,238 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /authenticators/admin/endpoint/: + get: + operationId: authenticators_admin_endpoint_list + description: Viewset for Endpoint authenticator devices (for admins) + parameters: + - in: query + name: name + 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: + - authenticators + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedEndpointDeviceList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: authenticators_admin_endpoint_create + description: Viewset for Endpoint authenticator devices (for admins) + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EndpointDeviceRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/EndpointDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /authenticators/admin/endpoint/{uuid}/: + get: + operationId: authenticators_admin_endpoint_retrieve + description: Viewset for Endpoint authenticator devices (for admins) + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Endpoint Device. + required: true + tags: + - authenticators + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EndpointDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: authenticators_admin_endpoint_update + description: Viewset for Endpoint authenticator devices (for admins) + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Endpoint Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EndpointDeviceRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EndpointDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: authenticators_admin_endpoint_partial_update + description: Viewset for Endpoint authenticator devices (for admins) + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Endpoint Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedEndpointDeviceRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EndpointDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: authenticators_admin_endpoint_destroy + description: Viewset for Endpoint authenticator devices (for admins) + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Endpoint Device. + required: true + tags: + - authenticators + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' /authenticators/admin/sms/: get: operationId: authenticators_admin_sms_list @@ -1809,10 +2041,10 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /authenticators/sms/: + /authenticators/endpoint/: get: - operationId: authenticators_sms_list - description: Viewset for sms authenticator devices + operationId: authenticators_endpoint_list + description: Viewset for Endpoint authenticator devices parameters: - in: query name: name @@ -1851,7 +2083,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedSMSDeviceList' + $ref: '#/components/schemas/PaginatedEndpointDeviceList' description: '' '400': content: @@ -1865,16 +2097,17 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /authenticators/sms/{id}/: + /authenticators/endpoint/{uuid}/: get: - operationId: authenticators_sms_retrieve - description: Viewset for sms authenticator devices + operationId: authenticators_endpoint_retrieve + description: Viewset for Endpoint authenticator devices parameters: - in: path - name: id + name: uuid schema: - type: integer - description: A unique integer value identifying this SMS Device. + type: string + format: uuid + description: A UUID string identifying this Endpoint Device. required: true tags: - authenticators @@ -1885,7 +2118,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SMSDevice' + $ref: '#/components/schemas/EndpointDevice' description: '' '400': content: @@ -1899,24 +2132,20 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - put: - operationId: authenticators_sms_update - description: Viewset for sms authenticator devices + /authenticators/endpoint/{uuid}/used_by/: + get: + operationId: authenticators_endpoint_used_by_list + description: Get a list of all objects that use this object parameters: - in: path - name: id + name: uuid schema: - type: integer - description: A unique integer value identifying this SMS Device. + type: string + format: uuid + description: A UUID string identifying this Endpoint Device. required: true tags: - authenticators - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/SMSDeviceRequest' - required: true security: - authentik: [] responses: @@ -1924,7 +2153,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SMSDevice' + type: array + items: + $ref: '#/components/schemas/UsedBy' description: '' '400': content: @@ -1938,8 +2169,65 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - patch: - operationId: authenticators_sms_partial_update + /authenticators/sms/: + get: + operationId: authenticators_sms_list + description: Viewset for sms authenticator devices + parameters: + - in: query + name: name + 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: + - authenticators + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSMSDeviceList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /authenticators/sms/{id}/: + get: + operationId: authenticators_sms_retrieve description: Viewset for sms authenticator devices parameters: - in: path @@ -1950,11 +2238,83 @@ paths: required: true tags: - authenticators - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedSMSDeviceRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SMSDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: authenticators_sms_update + description: Viewset for sms authenticator devices + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SMS Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SMSDeviceRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SMSDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: authenticators_sms_partial_update + description: Viewset for sms authenticator devices + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this SMS Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSMSDeviceRequest' security: - authentik: [] responses: @@ -22725,6 +23085,7 @@ paths: - authentik_sources_scim.scimsourcepropertymapping - authentik_stages_authenticator_duo.authenticatorduostage - authentik_stages_authenticator_duo.duodevice + - authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage - authentik_stages_authenticator_sms.authenticatorsmsstage - authentik_stages_authenticator_sms.smsdevice - authentik_stages_authenticator_static.authenticatorstaticstage @@ -22959,6 +23320,7 @@ paths: - authentik_sources_scim.scimsourcepropertymapping - authentik_stages_authenticator_duo.authenticatorduostage - authentik_stages_authenticator_duo.duodevice + - authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage - authentik_stages_authenticator_sms.authenticatorsmsstage - authentik_stages_authenticator_sms.smsdevice - authentik_stages_authenticator_static.authenticatorstaticstage @@ -28705,7 +29067,380 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedAuthenticatorDuoStageList' + $ref: '#/components/schemas/PaginatedAuthenticatorDuoStageList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + post: + operationId: stages_authenticator_duo_create + description: AuthenticatorDuoStage Viewset + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStageRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStage' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/duo/{stage_uuid}/: + get: + operationId: stages_authenticator_duo_retrieve + description: AuthenticatorDuoStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStage' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: stages_authenticator_duo_update + description: AuthenticatorDuoStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStageRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStage' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: stages_authenticator_duo_partial_update + description: AuthenticatorDuoStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAuthenticatorDuoStageRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStage' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: stages_authenticator_duo_destroy + description: AuthenticatorDuoStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/duo/{stage_uuid}/enrollment_status/: + post: + operationId: stages_authenticator_duo_enrollment_status_create + description: Check enrollment status of user details in current session + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DuoDeviceEnrollmentStatus' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/duo/{stage_uuid}/import_device_manual/: + post: + operationId: stages_authenticator_duo_import_device_manual_create + description: Import duo devices into authentik + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStageManualDeviceImportRequest' + required: true + security: + - authentik: [] + responses: + '204': + description: Enrollment successful + '400': + description: Bad request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/duo/{stage_uuid}/import_devices_automatic/: + post: + operationId: stages_authenticator_duo_import_devices_automatic_create + description: Import duo devices into authentik + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStageDeviceImportResponse' + description: '' + '400': + description: Bad request + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/duo/{stage_uuid}/used_by/: + get: + operationId: stages_authenticator_duo_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/endpoint_gdtc/: + get: + operationId: stages_authenticator_endpoint_gdtc_list + description: AuthenticatorEndpointGDTCStage Viewset + parameters: + - in: query + name: configure_flow + schema: + type: string + format: uuid + - in: query + name: name + 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/PaginatedAuthenticatorEndpointGDTCStageList' description: '' '400': content: @@ -28720,15 +29455,15 @@ paths: $ref: '#/components/schemas/GenericError' description: '' post: - operationId: stages_authenticator_duo_create - description: AuthenticatorDuoStage Viewset + operationId: stages_authenticator_endpoint_gdtc_create + description: AuthenticatorEndpointGDTCStage Viewset tags: - stages requestBody: content: application/json: schema: - $ref: '#/components/schemas/AuthenticatorDuoStageRequest' + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStageRequest' required: true security: - authentik: [] @@ -28737,7 +29472,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthenticatorDuoStage' + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStage' description: '' '400': content: @@ -28751,17 +29486,18 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /stages/authenticator/duo/{stage_uuid}/: + /stages/authenticator/endpoint_gdtc/{stage_uuid}/: get: - operationId: stages_authenticator_duo_retrieve - description: AuthenticatorDuoStage Viewset + operationId: stages_authenticator_endpoint_gdtc_retrieve + description: AuthenticatorEndpointGDTCStage Viewset parameters: - in: path name: stage_uuid schema: type: string format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. + description: A UUID string identifying this Endpoint Authenticator Google + Device Trust Connector Stage. required: true tags: - stages @@ -28772,7 +29508,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthenticatorDuoStage' + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStage' description: '' '400': content: @@ -28787,15 +29523,16 @@ paths: $ref: '#/components/schemas/GenericError' description: '' put: - operationId: stages_authenticator_duo_update - description: AuthenticatorDuoStage Viewset + operationId: stages_authenticator_endpoint_gdtc_update + description: AuthenticatorEndpointGDTCStage Viewset parameters: - in: path name: stage_uuid schema: type: string format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. + description: A UUID string identifying this Endpoint Authenticator Google + Device Trust Connector Stage. required: true tags: - stages @@ -28803,7 +29540,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthenticatorDuoStageRequest' + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStageRequest' required: true security: - authentik: [] @@ -28812,7 +29549,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthenticatorDuoStage' + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStage' description: '' '400': content: @@ -28827,15 +29564,16 @@ paths: $ref: '#/components/schemas/GenericError' description: '' patch: - operationId: stages_authenticator_duo_partial_update - description: AuthenticatorDuoStage Viewset + operationId: stages_authenticator_endpoint_gdtc_partial_update + description: AuthenticatorEndpointGDTCStage Viewset parameters: - in: path name: stage_uuid schema: type: string format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. + description: A UUID string identifying this Endpoint Authenticator Google + Device Trust Connector Stage. required: true tags: - stages @@ -28843,7 +29581,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PatchedAuthenticatorDuoStageRequest' + $ref: '#/components/schemas/PatchedAuthenticatorEndpointGDTCStageRequest' security: - authentik: [] responses: @@ -28851,7 +29589,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthenticatorDuoStage' + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStage' description: '' '400': content: @@ -28866,15 +29604,16 @@ paths: $ref: '#/components/schemas/GenericError' description: '' delete: - operationId: stages_authenticator_duo_destroy - description: AuthenticatorDuoStage Viewset + operationId: stages_authenticator_endpoint_gdtc_destroy + description: AuthenticatorEndpointGDTCStage Viewset parameters: - in: path name: stage_uuid schema: type: string format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. + description: A UUID string identifying this Endpoint Authenticator Google + Device Trust Connector Stage. required: true tags: - stages @@ -28895,108 +29634,9 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /stages/authenticator/duo/{stage_uuid}/enrollment_status/: - post: - operationId: stages_authenticator_duo_enrollment_status_create - description: Check enrollment status of user details in current session - parameters: - - in: path - name: stage_uuid - schema: - type: string - format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. - required: true - tags: - - stages - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DuoDeviceEnrollmentStatus' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /stages/authenticator/duo/{stage_uuid}/import_device_manual/: - post: - operationId: stages_authenticator_duo_import_device_manual_create - description: Import duo devices into authentik - parameters: - - in: path - name: stage_uuid - schema: - type: string - format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. - required: true - tags: - - stages - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/AuthenticatorDuoStageManualDeviceImportRequest' - required: true - security: - - authentik: [] - responses: - '204': - description: Enrollment successful - '400': - description: Bad request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /stages/authenticator/duo/{stage_uuid}/import_devices_automatic/: - post: - operationId: stages_authenticator_duo_import_devices_automatic_create - description: Import duo devices into authentik - parameters: - - in: path - name: stage_uuid - schema: - type: string - format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. - required: true - tags: - - stages - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/AuthenticatorDuoStageDeviceImportResponse' - description: '' - '400': - description: Bad request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' - /stages/authenticator/duo/{stage_uuid}/used_by/: + /stages/authenticator/endpoint_gdtc/{stage_uuid}/used_by/: get: - operationId: stages_authenticator_duo_used_by_list + operationId: stages_authenticator_endpoint_gdtc_used_by_list description: Get a list of all objects that use this object parameters: - in: path @@ -29004,7 +29644,8 @@ paths: schema: type: string format: uuid - description: A UUID string identifying this Duo Authenticator Setup Stage. + description: A UUID string identifying this Endpoint Authenticator Google + Device Trust Connector Stage. required: true tags: - stages @@ -35915,6 +36556,7 @@ components: - authentik.enterprise.providers.google_workspace - authentik.enterprise.providers.microsoft_entra - authentik.enterprise.providers.rac + - authentik.enterprise.stages.authenticator_endpoint_gdtc - authentik.enterprise.stages.source - authentik.events type: string @@ -36380,6 +37022,80 @@ components: - client_id - client_secret - name + AuthenticatorEndpointGDTCStage: + type: object + description: AuthenticatorEndpointGDTCStage 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 + credentials: {} + required: + - component + - credentials + - meta_model_name + - name + - pk + - verbose_name + - verbose_name_plural + AuthenticatorEndpointGDTCStageRequest: + type: object + description: AuthenticatorEndpointGDTCStage 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 + credentials: {} + required: + - credentials + - name AuthenticatorSMSChallenge: type: object description: SMS Setup challenge @@ -37629,6 +38345,7 @@ components: - $ref: '#/components/schemas/DummyChallenge' - $ref: '#/components/schemas/EmailChallenge' - $ref: '#/components/schemas/FlowErrorChallenge' + - $ref: '#/components/schemas/FrameChallenge' - $ref: '#/components/schemas/IdentificationChallenge' - $ref: '#/components/schemas/OAuthDeviceCodeChallenge' - $ref: '#/components/schemas/OAuthDeviceCodeFinishChallenge' @@ -37656,6 +38373,7 @@ components: ak-stage-dummy: '#/components/schemas/DummyChallenge' ak-stage-email: '#/components/schemas/EmailChallenge' ak-stage-flow-error: '#/components/schemas/FlowErrorChallenge' + xak-flow-frame: '#/components/schemas/FrameChallenge' ak-stage-identification: '#/components/schemas/IdentificationChallenge' ak-provider-oauth2-device-code: '#/components/schemas/OAuthDeviceCodeChallenge' ak-provider-oauth2-device-code-finish: '#/components/schemas/OAuthDeviceCodeFinishChallenge' @@ -38316,7 +39034,7 @@ components: description: Return internal model name readOnly: true pk: - type: integer + type: string name: type: string type: @@ -38929,6 +39647,35 @@ components: - protocol - provider - provider_obj + EndpointDevice: + type: object + description: Serializer for Endpoint authenticator devices + properties: + pk: + type: string + format: uuid + title: Uuid + name: + type: string + description: The human-readable name of this device. + maxLength: 64 + required: + - name + EndpointDeviceRequest: + type: object + description: Serializer for Endpoint authenticator devices + properties: + pk: + type: string + format: uuid + title: Uuid + name: + type: string + minLength: 1 + description: The human-readable name of this device. + maxLength: 64 + required: + - name EndpointRequest: type: object description: Endpoint Serializer @@ -39525,6 +40272,7 @@ components: - $ref: '#/components/schemas/ConsentChallengeResponseRequest' - $ref: '#/components/schemas/DummyChallengeResponseRequest' - $ref: '#/components/schemas/EmailChallengeResponseRequest' + - $ref: '#/components/schemas/FrameChallengeResponseRequest' - $ref: '#/components/schemas/IdentificationChallengeResponseRequest' - $ref: '#/components/schemas/OAuthDeviceCodeChallengeResponseRequest' - $ref: '#/components/schemas/OAuthDeviceCodeFinishChallengeResponseRequest' @@ -39547,6 +40295,7 @@ components: ak-stage-consent: '#/components/schemas/ConsentChallengeResponseRequest' ak-stage-dummy: '#/components/schemas/DummyChallengeResponseRequest' ak-stage-email: '#/components/schemas/EmailChallengeResponseRequest' + xak-flow-frame: '#/components/schemas/FrameChallengeResponseRequest' ak-stage-identification: '#/components/schemas/IdentificationChallengeResponseRequest' ak-provider-oauth2-device-code: '#/components/schemas/OAuthDeviceCodeChallengeResponseRequest' ak-provider-oauth2-device-code-finish: '#/components/schemas/OAuthDeviceCodeFinishChallengeResponseRequest' @@ -39903,6 +40652,39 @@ components: required: - href - name + FrameChallenge: + type: object + description: Challenge type to render a frame + properties: + flow_info: + $ref: '#/components/schemas/ContextualFlowInfo' + component: + type: string + default: xak-flow-frame + response_errors: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/ErrorDetail' + url: + type: string + loading_overlay: + type: boolean + default: false + loading_text: + type: string + required: + - loading_text + - url + FrameChallengeResponseRequest: + type: object + description: Base class for all challenge responses + properties: + component: + type: string + minLength: 1 + default: xak-flow-frame GenericError: type: object description: Generic API Error @@ -42142,6 +42924,7 @@ components: - authentik_providers_rac.racprovider - authentik_providers_rac.endpoint - authentik_providers_rac.racpropertymapping + - authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage - authentik_stages_source.sourcestage - authentik_events.event - authentik_events.notificationtransport @@ -43235,6 +44018,18 @@ components: required: - pagination - results + PaginatedAuthenticatorEndpointGDTCStageList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/AuthenticatorEndpointGDTCStage' + required: + - pagination + - results PaginatedAuthenticatorSMSStageList: type: object properties: @@ -43451,6 +44246,18 @@ components: required: - pagination - results + PaginatedEndpointDeviceList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/EndpointDevice' + required: + - pagination + - results PaginatedEndpointList: type: object properties: @@ -45088,6 +45895,28 @@ components: admin_secret_key: type: string writeOnly: true + PatchedAuthenticatorEndpointGDTCStageRequest: + type: object + description: AuthenticatorEndpointGDTCStage 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 + credentials: {} PatchedAuthenticatorSMSStageRequest: type: object description: AuthenticatorSMSStage Serializer @@ -45569,6 +46398,19 @@ components: activate_user_on_success: type: boolean description: Activate users upon completion of stage. + PatchedEndpointDeviceRequest: + type: object + description: Serializer for Endpoint authenticator devices + properties: + pk: + type: string + format: uuid + title: Uuid + name: + type: string + minLength: 1 + description: The human-readable name of this device. + maxLength: 64 PatchedEndpointRequest: type: object description: Endpoint Serializer diff --git a/web/src/admin/stages/StageListPage.ts b/web/src/admin/stages/StageListPage.ts index 5d253f41c0c7..7d99e1550241 100644 --- a/web/src/admin/stages/StageListPage.ts +++ b/web/src/admin/stages/StageListPage.ts @@ -2,6 +2,7 @@ import "@goauthentik/admin/rbac/ObjectPermissionModal"; import "@goauthentik/admin/stages/StageWizard"; import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm"; import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm"; +import "@goauthentik/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm"; import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm"; import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm"; import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm"; @@ -25,8 +26,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ProxyForm"; -import { PaginatedResponse } from "@goauthentik/elements/table/Table"; -import { TableColumn } from "@goauthentik/elements/table/Table"; +import { PaginatedResponse, TableColumn } from "@goauthentik/elements/table/Table"; import { TablePage } from "@goauthentik/elements/table/TablePage"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; diff --git a/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts b/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts new file mode 100644 index 000000000000..414e42d14753 --- /dev/null +++ b/web/src/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm.ts @@ -0,0 +1,75 @@ +import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/elements/CodeMirror"; +import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { AuthenticatorEndpointGDTCStage, StagesApi } from "@goauthentik/api"; + +@customElement("ak-stage-authenticator-endpoint-gdtc-form") +export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm { + loadInstance(pk: string): Promise { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEndpointGdtcRetrieve({ + stageUuid: pk, + }); + } + + async send(data: AuthenticatorEndpointGDTCStage): Promise { + if (this.instance) { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEndpointGdtcPartialUpdate({ + stageUuid: this.instance.pk || "", + patchedAuthenticatorEndpointGDTCStageRequest: data, + }); + } else { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEndpointGdtcCreate({ + authenticatorEndpointGDTCStageRequest: data, + }); + } + } + + renderForm(): TemplateResult { + return html` + ${msg( + "Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows.", + )} + + + + + + ${msg("Google Verified Access API")} +
+ + +

+ ${msg("Google Cloud credentials file.")} +

+
+
+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-stage-authenticator-endpoint-gdtc-form": AuthenticatorEndpointGDTCStageForm; + } +} diff --git a/web/src/admin/users/UserDevicesTable.ts b/web/src/admin/users/UserDevicesTable.ts index 70ccb396f006..241f8d020f27 100644 --- a/web/src/admin/users/UserDevicesTable.ts +++ b/web/src/admin/users/UserDevicesTable.ts @@ -1,11 +1,12 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { SentryIgnoredError } from "@goauthentik/common/errors"; import { deviceTypeName } from "@goauthentik/common/labels"; import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/elements/forms/DeleteBulkForm"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; -import { msg } from "@lit/localize"; +import { msg, str } from "@lit/localize"; import { TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; @@ -54,20 +55,21 @@ export class UserDeviceTable extends Table { async deleteWrapper(device: Device) { const api = new AuthenticatorsApi(DEFAULT_CONFIG); - const id = { id: device.pk }; switch (device.type) { case "authentik_stages_authenticator_duo.DuoDevice": - return api.authenticatorsAdminDuoDestroy(id); + return api.authenticatorsAdminDuoDestroy({ id: parseInt(device.pk, 10) }); case "authentik_stages_authenticator_sms.SMSDevice": - return api.authenticatorsAdminSmsDestroy(id); + return api.authenticatorsAdminSmsDestroy({ id: parseInt(device.pk, 10) }); case "authentik_stages_authenticator_totp.TOTPDevice": - return api.authenticatorsAdminTotpDestroy(id); + return api.authenticatorsAdminTotpDestroy({ id: parseInt(device.pk, 10) }); case "authentik_stages_authenticator_static.StaticDevice": - return api.authenticatorsAdminStaticDestroy(id); + return api.authenticatorsAdminStaticDestroy({ id: parseInt(device.pk, 10) }); case "authentik_stages_authenticator_webauthn.WebAuthnDevice": - return api.authenticatorsAdminWebauthnDestroy(id); + return api.authenticatorsAdminWebauthnDestroy({ id: parseInt(device.pk, 10) }); default: - break; + throw new SentryIgnoredError( + msg(str`Device type ${device.verboseName} cannot be deleted`), + ); } } diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index 4dcf5c6f9bbe..fabfe35babd5 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -16,6 +16,7 @@ import { themeImage } from "@goauthentik/elements/utils/images"; import "@goauthentik/flow/sources/apple/AppleLoginInit"; import "@goauthentik/flow/sources/plex/PlexLoginInit"; import "@goauthentik/flow/stages/FlowErrorStage"; +import "@goauthentik/flow/stages/FlowFrameStage"; import "@goauthentik/flow/stages/RedirectStage"; import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base"; @@ -170,6 +171,19 @@ export class FlowExecutor extends Interface implements StageHost { this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => { this.inspectorOpen = !this.inspectorOpen; }); + window.addEventListener("message", (event) => { + const msg: { + source?: string; + context?: string; + message: string; + } = event.data; + if (msg.source !== "goauthentik.io" || msg.context !== "flow-executor") { + return; + } + if (msg.message === "submit") { + this.submit({} as FlowChallengeResponseRequest); + } + }); } async getTheme(): Promise { @@ -429,6 +443,11 @@ export class FlowExecutor extends Interface implements StageHost { `; case "xak-flow-shell": return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`; + case "xak-flow-frame": + return html``; default: return html`Invalid native challenge element`; } diff --git a/web/src/flow/components/ak-flow-password-input.ts b/web/src/flow/components/ak-flow-password-input.ts index 19e704fa29f6..a4190a2519b0 100644 --- a/web/src/flow/components/ak-flow-password-input.ts +++ b/web/src/flow/components/ak-flow-password-input.ts @@ -114,7 +114,7 @@ export class InputPassword extends AKElement { this.input.type = "password"; this.input.name = this.name; this.input.placeholder = this.placeholder; - this.input.autofocus = true; + this.input.autofocus = this.grabFocus; this.input.autocomplete = "current-password"; this.input.classList.add("pf-c-form-control"); this.input.required = true; diff --git a/web/src/flow/stages/FlowFrameStage.ts b/web/src/flow/stages/FlowFrameStage.ts new file mode 100644 index 000000000000..7372f256a9b3 --- /dev/null +++ b/web/src/flow/stages/FlowFrameStage.ts @@ -0,0 +1,54 @@ +import "@goauthentik/elements/EmptyState"; +import "@goauthentik/flow/FormStatic"; +import { BaseStage } from "@goauthentik/flow/stages/base"; + +import { CSSResult, TemplateResult, css, html, nothing } from "lit"; +import { customElement } from "lit/decorators.js"; + +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFLogin from "@patternfly/patternfly/components/Login/login.css"; +import PFTitle from "@patternfly/patternfly/components/Title/title.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { FrameChallenge, FrameChallengeResponseRequest } from "@goauthentik/api"; + +@customElement("xak-flow-frame") +export class FlowFrameStage extends BaseStage { + static get styles(): CSSResult[] { + return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, css``]; + } + + render(): TemplateResult { + if (!this.challenge) { + return html` `; + } + return html` + +
+ +
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + "xak-flow-frame": FlowFrameStage; + } +} diff --git a/web/src/user/user-settings/mfa/MFADeviceForm.ts b/web/src/user/user-settings/mfa/MFADeviceForm.ts index c470d32bb3fb..7ac6f9e4d7ab 100644 --- a/web/src/user/user-settings/mfa/MFADeviceForm.ts +++ b/web/src/user/user-settings/mfa/MFADeviceForm.ts @@ -1,8 +1,9 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { SentryIgnoredError } from "@goauthentik/common/errors"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; -import { msg } from "@lit/localize"; +import { msg, str } from "@lit/localize"; import { TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -10,11 +11,11 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { AuthenticatorsApi, Device } from "@goauthentik/api"; @customElement("ak-user-mfa-form") -export class MFADeviceForm extends ModelForm { +export class MFADeviceForm extends ModelForm { @property() deviceType!: string; - async loadInstance(pk: number): Promise { + async loadInstance(pk: string): Promise { const devices = await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsAllList(); return devices.filter((device) => { return device.pk === pk && device.type === this.deviceType; @@ -29,36 +30,38 @@ export class MFADeviceForm extends ModelForm { switch (this.instance?.type) { case "authentik_stages_authenticator_duo.DuoDevice": await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsDuoUpdate({ - id: this.instance?.pk, + id: parseInt(this.instance?.pk, 10), duoDeviceRequest: device, }); break; case "authentik_stages_authenticator_sms.SMSDevice": await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsSmsUpdate({ - id: this.instance?.pk, + id: parseInt(this.instance?.pk, 10), sMSDeviceRequest: device, }); break; case "authentik_stages_authenticator_totp.TOTPDevice": await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsTotpUpdate({ - id: this.instance?.pk, + id: parseInt(this.instance?.pk, 10), tOTPDeviceRequest: device, }); break; case "authentik_stages_authenticator_static.StaticDevice": await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticUpdate({ - id: this.instance?.pk, + id: parseInt(this.instance?.pk, 10), staticDeviceRequest: device, }); break; case "authentik_stages_authenticator_webauthn.WebAuthnDevice": await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnUpdate({ - id: this.instance?.pk, + id: parseInt(this.instance?.pk, 10), webAuthnDeviceRequest: device, }); break; default: - break; + throw new SentryIgnoredError( + msg(str`Device type ${device.verboseName} cannot be edited`), + ); } return device; } diff --git a/web/src/user/user-settings/mfa/MFADevicesPage.ts b/web/src/user/user-settings/mfa/MFADevicesPage.ts index 85e72094e182..29a82c48e373 100644 --- a/web/src/user/user-settings/mfa/MFADevicesPage.ts +++ b/web/src/user/user-settings/mfa/MFADevicesPage.ts @@ -1,4 +1,5 @@ import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { SentryIgnoredError } from "@goauthentik/common/errors"; import { deviceTypeName } from "@goauthentik/common/labels"; import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/elements/buttons/Dropdown"; @@ -10,7 +11,7 @@ import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/tab import "@goauthentik/user/user-settings/mfa/MFADeviceForm"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; -import { msg } from "@lit/localize"; +import { msg, str } from "@lit/localize"; import { TemplateResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -89,7 +90,7 @@ export class MFADevicesPage extends Table { async deleteWrapper(device: Device) { const api = new AuthenticatorsApi(DEFAULT_CONFIG); - const id = { id: device.pk }; + const id = { id: parseInt(device.pk, 10) }; switch (device.type) { case "authentik_stages_authenticator_duo.DuoDevice": return api.authenticatorsDuoDestroy(id); @@ -102,7 +103,9 @@ export class MFADevicesPage extends Table { case "authentik_stages_authenticator_webauthn.WebAuthnDevice": return api.authenticatorsWebauthnDestroy(id); default: - break; + throw new SentryIgnoredError( + msg(str`Device type ${device.verboseName} cannot be deleted`), + ); } } diff --git a/website/docs/add-secure-apps/flows-stages/stages/authenticator_endpoint_gdtc/index.md b/website/docs/add-secure-apps/flows-stages/stages/authenticator_endpoint_gdtc/index.md new file mode 100644 index 000000000000..096941790481 --- /dev/null +++ b/website/docs/add-secure-apps/flows-stages/stages/authenticator_endpoint_gdtc/index.md @@ -0,0 +1,78 @@ +--- +title: Endpoint Authenticator Google Device Trust Connector Stage +--- + +Enterprise +authentik 2024.10+ + +--- + +With this stage, authentik can validate users' Chrome browsers and ensure that users' devices are compliant and up-to-date. + +:::info +This stage only works with Google Chrome, as it relies on the [Chrome Verified Access API](https://developers.google.com/chrome/verified-access). +::: + +## Configuration + +The main steps to set up your Google workspace are as follows: + +1. [Create your Google Cloud Project](#create-a-google-cloud-project) +2. [Create a service account](#create-a-service-account) +3. [Set credentials for the service account](#set-credentials-for-the-service-account) +4. [Define access and scope in the Admin Console](#set-credentials-for-the-service-account) + +For detailed instructions, refer to Google documentation. + +### Create a Google cloud project + +1. Open the Google Cloud Console (https://cloud.google.com/cloud-console). +2. In upper left, click the drop-down box to open the **Select a project** modal box, and then select **New Project**. +3. Create a new project and give it a name like "authentik GWS". +4. Use the search bar at the top of your new project page to search for "API Library". +5. On the **API Library** page, use the search bar again to find "Chrome Verified Access API". +6. On the **Chrome Verified Access API** page, click **Enable**. + +### Create a service account + +1. After the new Chrome Verified Access API is enabled (it might take a few minutes), return to the Google Cloud console home page (click on **Google Cloud** in upper left). +2. Use the search bar to find and navigate to the **IAM** page. +3. On the **IAM** page, click **Service Accounts** in the left navigation pane. +4. At the top of the **Service Accounts** page, click **Create Service Account**. + +- Under **Service account details** page, define the **Name** and **Description** for the new service account, and then click **Create and Continue**. +- Under **Grant this service account access to project** you do not need to define a role, so click **Continue**. +- Under **Grant users access to project** you do not need to define a role, so click **Done** to complete the creation of the service account. + +### Set credentials for the service account + +1. On the **Service accounts** page, click the account that you just created. +2. Click the **Keys** tab at top of the page, the click **Add Key -> Create new key**. +3. In the Create modal box, select JSON as the key type, and then click **Create**. + A pop-up displays with the private key, and the key is saved to your computer as a JSON file. + Later, when you create the stage in authentik, you will add this key in the **Credentials** field. +4. On the service account page, click the **Details** tab, and expand the **Advanced settings** area. +5. Log in to the Admin Console, and then navigate to **Chrome browser -> Connectors**. +6. Click on **New Provider Configuration**. +7. Under Okta, click "Set up". +8. Enter a name. +9. Enter the URL: https://authentik.company/endpoint/gdtc/chrome/ +10. Under Service accounts, enter the full name of the service account created above, for example `authentik-gdtc-docs@authentik-enterprise-dev.iam.gserviceaccount.com`. + +### Create the stage + +1. Log in as an admin to authentik, and go to the Admin interface. + +2. In the Admin interface, navigate to **Flows -> Stages**. + +3. Click **Create**, and select **Endpoint Authenticator Google Device Trust Connector Stage**, and in the **New stage** modal box, define the following fields: + + - **Name**: define a descriptive name, such as "chrome-device-trust". + + - **Google Verified Access API** + + - **Credentials**: paste the contents of the JSON file (the key) that you downloaded earlier. + +4. Click **Finish**. + +After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.md), this stage only verifies the users' browser. diff --git a/website/sidebars.js b/website/sidebars.js index acca54bf1dc2..f45c88cdb879 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -282,6 +282,7 @@ export default { }, items: [ "add-secure-apps/flows-stages/stages/authenticator_duo/index", + "add-secure-apps/flows-stages/stages/authenticator_endpoint_gdtc/index", "add-secure-apps/flows-stages/stages/authenticator_sms/index", "add-secure-apps/flows-stages/stages/authenticator_static/index", "add-secure-apps/flows-stages/stages/authenticator_totp/index",