diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 526cf27ce..20b81a9cd 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -84,6 +84,7 @@ INSTALLED_APPS = [ "authentik.sources.saml", "authentik.stages.authenticator", "authentik.stages.authenticator_duo", + "authentik.stages.authenticator_mobile", "authentik.stages.authenticator_sms", "authentik.stages.authenticator_static", "authentik.stages.authenticator_totp", diff --git a/authentik/stages/authenticator_mobile/__init__.py b/authentik/stages/authenticator_mobile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/stages/authenticator_mobile/api.py b/authentik/stages/authenticator_mobile/api.py new file mode 100644 index 000000000..dd6be6c96 --- /dev/null +++ b/authentik/stages/authenticator_mobile/api.py @@ -0,0 +1,92 @@ +"""AuthenticatorDuoStage API Views""" +from django.http import Http404 +from django_filters.rest_framework.backends import DjangoFilterBackend +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer +from guardian.shortcuts import get_objects_for_user +from rest_framework import mixins +from rest_framework.decorators import action +from rest_framework.fields import CharField, ChoiceField, IntegerField +from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.permissions import IsAdminUser +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from authentik.api.authorization import OwnerFilter, OwnerPermissions +from authentik.api.decorators import permission_required +from authentik.core.api.used_by import UsedByMixin +from authentik.flows.api.stages import StageSerializer +from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage, MobileDevice + + +class AuthenticatorMobileStageSerializer(StageSerializer): + """AuthenticatorMobileStage Serializer""" + + class Meta: + model = AuthenticatorMobileStage + fields = StageSerializer.Meta.fields + [ + "configure_flow", + "friendly_name", + ] + # extra_kwargs = { + # "client_secret": {"write_only": True}, + # "admin_secret_key": {"write_only": True}, + # } + + +class AuthenticatorMobileStageViewSet(UsedByMixin, ModelViewSet): + """AuthenticatorMobileStage Viewset""" + + queryset = AuthenticatorMobileStage.objects.all() + serializer_class = AuthenticatorMobileStageSerializer + filterset_fields = [ + "name", + "configure_flow", + ] + search_fields = ["name"] + ordering = ["name"] + + @action(methods=["GET"], detail=True) + def enrollment_callback(self, request: Request, pk: str) -> Response: + pass + + +class MobileDeviceSerializer(ModelSerializer): + """Serializer for Mobile authenticator devices""" + + class Meta: + model = MobileDevice + fields = ["pk", "name"] + depth = 2 + + +class MobileDeviceViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + UsedByMixin, + mixins.ListModelMixin, + GenericViewSet, +): + """Viewset for Mobile authenticator devices""" + + queryset = MobileDevice.objects.all() + serializer_class = MobileDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] + + +class AdminMobileDeviceViewSet(ModelViewSet): + """Viewset for Mobile authenticator devices (for admins)""" + + permission_classes = [IsAdminUser] + queryset = MobileDevice.objects.all() + serializer_class = MobileDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] diff --git a/authentik/stages/authenticator_mobile/apps.py b/authentik/stages/authenticator_mobile/apps.py new file mode 100644 index 000000000..b23fb43fa --- /dev/null +++ b/authentik/stages/authenticator_mobile/apps.py @@ -0,0 +1,12 @@ +"""authentik mobile app config""" + +from authentik.blueprints.apps import ManagedAppConfig + + +class AuthentikStageAuthenticatorMobileConfig(ManagedAppConfig): + """authentik mobile config""" + + name = "authentik.stages.authenticator_mobile" + label = "authentik_stages_authenticator_mobile" + verbose_name = "authentik Stages.Authenticator.Mobile" + default = True diff --git a/authentik/stages/authenticator_mobile/migrations/0001_initial.py b/authentik/stages/authenticator_mobile/migrations/0001_initial.py new file mode 100644 index 000000000..29705238e --- /dev/null +++ b/authentik/stages/authenticator_mobile/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 4.1.10 on 2023-07-24 18:48 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="AuthenticatorMobileStage", + 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)), + ( + "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": "Mobile Authenticator Setup Stage", + "verbose_name_plural": "Mobile Authenticator Setup Stages", + }, + bases=("authentik_flows.stage", models.Model), + ), + migrations.CreateModel( + name="MobileDevice", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "name", + models.CharField( + help_text="The human-readable name of this device.", max_length=64 + ), + ), + ( + "confirmed", + models.BooleanField(default=True, help_text="Is this device ready for use?"), + ), + ("device_id", models.TextField(unique=True)), + ( + "stage", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_stages_authenticator_mobile.authenticatormobilestage", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "verbose_name": "Mobile Device", + "verbose_name_plural": "Mobile Devices", + }, + ), + ] diff --git a/authentik/stages/authenticator_mobile/migrations/__init__.py b/authentik/stages/authenticator_mobile/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/stages/authenticator_mobile/models.py b/authentik/stages/authenticator_mobile/models.py new file mode 100644 index 000000000..d0bb2f824 --- /dev/null +++ b/authentik/stages/authenticator_mobile/models.py @@ -0,0 +1,72 @@ +"""Mobile authenticator stage""" +from typing import Optional + +from django.contrib.auth import get_user_model +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.views import View +from django_otp.models import Device +from rest_framework.serializers import BaseSerializer, Serializer + +from authentik.core.types import UserSettingSerializer +from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage +from authentik.lib.models import SerializerModel + + +class AuthenticatorMobileStage(ConfigurableStage, FriendlyNamedStage, Stage): + """Setup Duo authenticator devices""" + + @property + def serializer(self) -> type[BaseSerializer]: + from authentik.stages.authenticator_mobile.api import AuthenticatorMobileStageSerializer + + return AuthenticatorMobileStageSerializer + + @property + def type(self) -> type[View]: + from authentik.stages.authenticator_mobile.stage import AuthenticatorMobileStageView + + return AuthenticatorMobileStageView + + @property + def component(self) -> str: + return "ak-stage-authenticator-mobile-form" + + def ui_user_settings(self) -> Optional[UserSettingSerializer]: + return UserSettingSerializer( + data={ + "title": self.friendly_name or str(self._meta.verbose_name), + "component": "ak-user-settings-authenticator-mobile", + } + ) + + def __str__(self) -> str: + return f"Mobile Authenticator Setup Stage {self.name}" + + class Meta: + verbose_name = _("Mobile Authenticator Setup Stage") + verbose_name_plural = _("Mobile Authenticator Setup Stages") + + +class MobileDevice(SerializerModel, Device): + """Mobile authenticator for a single user""" + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + + # Connect to the stage to when validating access we know the API Credentials + stage = models.ForeignKey(AuthenticatorMobileStage, on_delete=models.CASCADE) + + device_id = models.TextField(unique=True) + + @property + def serializer(self) -> Serializer: + from authentik.stages.authenticator_mobile.api import MobileDeviceSerializer + + return MobileDeviceSerializer + + def __str__(self): + return str(self.name) or str(self.user) + + class Meta: + verbose_name = _("Mobile Device") + verbose_name_plural = _("Mobile Devices") diff --git a/authentik/stages/authenticator_mobile/stage.py b/authentik/stages/authenticator_mobile/stage.py new file mode 100644 index 000000000..0bf772dca --- /dev/null +++ b/authentik/stages/authenticator_mobile/stage.py @@ -0,0 +1,49 @@ +"""Mobile stage""" +from django.http import HttpResponse +from django.utils.timezone import now +from rest_framework.fields import CharField + +from authentik.events.models import Event, EventAction +from authentik.flows.challenge import ( + Challenge, + ChallengeResponse, + ChallengeTypes, + WithUserInfoChallenge, +) +from authentik.flows.stage import ChallengeStageView +from authentik.stages.authenticator_mobile.models import AuthenticatorMobileStage + +SESSION_KEY_MOBILE_ENROLL = "authentik/stages/authenticator_mobile/enroll" + + +class AuthenticatorMobileChallenge(WithUserInfoChallenge): + """Mobile Challenge""" + + authentik_url = CharField(required=True) + stage_uuid = CharField(required=True) + component = CharField(default="ak-stage-authenticator-mobile") + + +class AuthenticatorMobileChallengeResponse(ChallengeResponse): + """Pseudo class for mobile response""" + + component = CharField(default="ak-stage-authenticator-mobile") + + +class AuthenticatorMobileStageView(ChallengeStageView): + """Mobile stage""" + + response_class = AuthenticatorMobileChallengeResponse + + def get_challenge(self, *args, **kwargs) -> Challenge: + stage: AuthenticatorMobileStage = self.executor.current_stage + return AuthenticatorMobileChallenge( + data={ + "type": ChallengeTypes.NATIVE.value, + "authentik_url": self.request.get_host(), + "stage_uuid": str(stage.stage_uuid), + } + ) + + def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: + return self.executor.stage_ok() diff --git a/authentik/stages/authenticator_mobile/urls.py b/authentik/stages/authenticator_mobile/urls.py new file mode 100644 index 000000000..38ee44b13 --- /dev/null +++ b/authentik/stages/authenticator_mobile/urls.py @@ -0,0 +1,16 @@ +"""API URLs""" +from authentik.stages.authenticator_mobile.api import ( + AdminMobileDeviceViewSet, + AuthenticatorMobileStageViewSet, + MobileDeviceViewSet, +) + +api_urlpatterns = [ + ("authenticators/mobile", MobileDeviceViewSet), + ( + "authenticators/admin/mobile", + AdminMobileDeviceViewSet, + "admin-mobiledevice", + ), + ("stages/authenticator/mobile", AuthenticatorMobileStageViewSet), +] diff --git a/blueprints/schema.json b/blueprints/schema.json index bf66c94ed..5ea7c9f59 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -1595,6 +1595,78 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_stages_authenticator_mobile.authenticatormobilestage" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "present", + "created" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "attrs": { + "$ref": "#/$defs/model_authentik_stages_authenticator_mobile.authenticatormobilestage" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_stages_authenticator_mobile.authenticatormobilestage" + } + } + }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_stages_authenticator_mobile.mobiledevice" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "present", + "created" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "attrs": { + "$ref": "#/$defs/model_authentik_stages_authenticator_mobile.mobiledevice" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_stages_authenticator_mobile.mobiledevice" + } + } + }, { "type": "object", "required": [ @@ -3455,6 +3527,7 @@ "authentik.sources.saml", "authentik.stages.authenticator", "authentik.stages.authenticator_duo", + "authentik.stages.authenticator_mobile", "authentik.stages.authenticator_sms", "authentik.stages.authenticator_static", "authentik.stages.authenticator_totp", @@ -3530,6 +3603,8 @@ "authentik_sources_saml.usersamlsourceconnection", "authentik_stages_authenticator_duo.authenticatorduostage", "authentik_stages_authenticator_duo.duodevice", + "authentik_stages_authenticator_mobile.authenticatormobilestage", + "authentik_stages_authenticator_mobile.mobiledevice", "authentik_stages_authenticator_sms.authenticatorsmsstage", "authentik_stages_authenticator_sms.smsdevice", "authentik_stages_authenticator_static.authenticatorstaticstage", @@ -5843,6 +5918,125 @@ }, "required": [] }, + "model_authentik_stages_authenticator_mobile.authenticatormobilestage": { + "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": "integer", + "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" + } + }, + "required": [] + }, + "model_authentik_stages_authenticator_mobile.mobiledevice": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "title": "Name", + "description": "The human-readable name of this device." + } + }, + "required": [] + }, "model_authentik_stages_authenticator_sms.authenticatorsmsstage": { "type": "object", "properties": { diff --git a/schema.yml b/schema.yml index da1a90a18..d6e209690 100644 --- a/schema.yml +++ b/schema.yml @@ -560,6 +560,234 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /authenticators/admin/mobile/: + get: + operationId: authenticators_admin_mobile_list + description: Viewset for Mobile 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/PaginatedMobileDeviceList' + 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_mobile_create + description: Viewset for Mobile authenticator devices (for admins) + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /authenticators/admin/mobile/{id}/: + get: + operationId: authenticators_admin_mobile_retrieve + description: Viewset for Mobile authenticator devices (for admins) + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + 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_mobile_update + description: Viewset for Mobile authenticator devices (for admins) + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + 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_mobile_partial_update + description: Viewset for Mobile authenticator devices (for admins) + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMobileDeviceRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + 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_mobile_destroy + description: Viewset for Mobile authenticator devices (for admins) + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile 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 @@ -1733,6 +1961,238 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /authenticators/mobile/: + get: + operationId: authenticators_mobile_list + description: Viewset for Mobile 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/PaginatedMobileDeviceList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /authenticators/mobile/{id}/: + get: + operationId: authenticators_mobile_retrieve + description: Viewset for Mobile authenticator devices + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + put: + operationId: authenticators_mobile_update + description: Viewset for Mobile authenticator devices + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDeviceRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + patch: + operationId: authenticators_mobile_partial_update + description: Viewset for Mobile authenticator devices + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMobileDeviceRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/MobileDevice' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + delete: + operationId: authenticators_mobile_destroy + description: Viewset for Mobile authenticator devices + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile 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/mobile/{id}/used_by/: + get: + operationId: authenticators_mobile_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this Mobile Device. + required: true + tags: + - authenticators + 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: '' /authenticators/sms/: get: operationId: authenticators_sms_list @@ -21900,6 +22360,315 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' + /stages/authenticator/mobile/: + get: + operationId: stages_authenticator_mobile_list + description: AuthenticatorMobileStage 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/PaginatedAuthenticatorMobileStageList' + 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_mobile_create + description: AuthenticatorMobileStage Viewset + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStageRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStage' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/mobile/{stage_uuid}/: + get: + operationId: stages_authenticator_mobile_retrieve + description: AuthenticatorMobileStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Mobile Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStage' + 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_mobile_update + description: AuthenticatorMobileStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Mobile Authenticator Setup Stage. + required: true + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStageRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStage' + 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_mobile_partial_update + description: AuthenticatorMobileStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Mobile Authenticator Setup Stage. + required: true + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAuthenticatorMobileStageRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStage' + 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_mobile_destroy + description: AuthenticatorMobileStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Mobile 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/mobile/{stage_uuid}/enrollment_callback/: + get: + operationId: stages_authenticator_mobile_enrollment_callback_retrieve + description: AuthenticatorMobileStage Viewset + parameters: + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Mobile Authenticator Setup Stage. + required: true + tags: + - stages + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorMobileStage' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + description: '' + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + description: '' + /stages/authenticator/mobile/{stage_uuid}/used_by/: + get: + operationId: stages_authenticator_mobile_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 Mobile 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/sms/: get: operationId: stages_authenticator_sms_list @@ -27892,6 +28661,7 @@ components: - authentik.sources.saml - authentik.stages.authenticator - authentik.stages.authenticator_duo + - authentik.stages.authenticator_mobile - authentik.stages.authenticator_sms - authentik.stages.authenticator_static - authentik.stages.authenticator_totp @@ -27943,6 +28713,7 @@ components: * `authentik.sources.saml` - authentik Sources.SAML * `authentik.stages.authenticator` - authentik Stages.Authenticator * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_mobile` - authentik Stages.Authenticator.Mobile * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP @@ -28506,6 +29277,115 @@ components: - client_id - client_secret - name + AuthenticatorMobileChallenge: + type: object + description: Mobile Challenge + properties: + type: + $ref: '#/components/schemas/ChallengeChoices' + flow_info: + $ref: '#/components/schemas/ContextualFlowInfo' + component: + type: string + default: ak-stage-authenticator-mobile + response_errors: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/ErrorDetail' + pending_user: + type: string + pending_user_avatar: + type: string + authentik_url: + type: string + stage_uuid: + type: string + required: + - authentik_url + - pending_user + - pending_user_avatar + - stage_uuid + - type + AuthenticatorMobileChallengeResponseRequest: + type: object + description: Pseudo class for mobile response + properties: + component: + type: string + minLength: 1 + default: ak-stage-authenticator-mobile + AuthenticatorMobileStage: + type: object + description: AuthenticatorMobileStage 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 + required: + - component + - meta_model_name + - name + - pk + - verbose_name + - verbose_name_plural + AuthenticatorMobileStageRequest: + type: object + description: AuthenticatorMobileStage 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 + required: + - name AuthenticatorSMSChallenge: type: object description: SMS Setup challenge @@ -29541,6 +30421,7 @@ components: - $ref: '#/components/schemas/AccessDeniedChallenge' - $ref: '#/components/schemas/AppleLoginChallenge' - $ref: '#/components/schemas/AuthenticatorDuoChallenge' + - $ref: '#/components/schemas/AuthenticatorMobileChallenge' - $ref: '#/components/schemas/AuthenticatorSMSChallenge' - $ref: '#/components/schemas/AuthenticatorStaticChallenge' - $ref: '#/components/schemas/AuthenticatorTOTPChallenge' @@ -29567,6 +30448,7 @@ components: ak-stage-access-denied: '#/components/schemas/AccessDeniedChallenge' ak-source-oauth-apple: '#/components/schemas/AppleLoginChallenge' ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallenge' + ak-stage-authenticator-mobile: '#/components/schemas/AuthenticatorMobileChallenge' ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallenge' ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge' ak-stage-authenticator-totp: '#/components/schemas/AuthenticatorTOTPChallenge' @@ -30692,6 +31574,7 @@ components: * `authentik.sources.saml` - authentik Sources.SAML * `authentik.stages.authenticator` - authentik Stages.Authenticator * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_mobile` - authentik Stages.Authenticator.Mobile * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP @@ -30763,6 +31646,8 @@ components: * `authentik_sources_saml.usersamlsourceconnection` - User SAML Source Connection * `authentik_stages_authenticator_duo.authenticatorduostage` - Duo Authenticator Setup Stage * `authentik_stages_authenticator_duo.duodevice` - Duo Device + * `authentik_stages_authenticator_mobile.authenticatormobilestage` - Mobile Authenticator Setup Stage + * `authentik_stages_authenticator_mobile.mobiledevice` - Mobile Device * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage * `authentik_stages_authenticator_sms.smsdevice` - SMS Device * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage @@ -30888,6 +31773,7 @@ components: * `authentik.sources.saml` - authentik Sources.SAML * `authentik.stages.authenticator` - authentik Stages.Authenticator * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_mobile` - authentik Stages.Authenticator.Mobile * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP @@ -30959,6 +31845,8 @@ components: * `authentik_sources_saml.usersamlsourceconnection` - User SAML Source Connection * `authentik_stages_authenticator_duo.authenticatorduostage` - Duo Authenticator Setup Stage * `authentik_stages_authenticator_duo.duodevice` - Duo Device + * `authentik_stages_authenticator_mobile.authenticatormobilestage` - Mobile Authenticator Setup Stage + * `authentik_stages_authenticator_mobile.mobiledevice` - Mobile Device * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage * `authentik_stages_authenticator_sms.smsdevice` - SMS Device * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage @@ -31347,6 +32235,7 @@ components: oneOf: - $ref: '#/components/schemas/AppleChallengeResponseRequest' - $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest' + - $ref: '#/components/schemas/AuthenticatorMobileChallengeResponseRequest' - $ref: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest' - $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest' - $ref: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest' @@ -31369,6 +32258,7 @@ components: mapping: ak-source-oauth-apple: '#/components/schemas/AppleChallengeResponseRequest' ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest' + ak-stage-authenticator-mobile: '#/components/schemas/AuthenticatorMobileChallengeResponseRequest' ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest' ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest' ak-stage-authenticator-totp: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest' @@ -33218,6 +34108,32 @@ components: required: - labels - name + MobileDevice: + type: object + description: Serializer for Mobile authenticator devices + properties: + pk: + type: integer + readOnly: true + title: ID + name: + type: string + description: The human-readable name of this device. + maxLength: 64 + required: + - name + - pk + MobileDeviceRequest: + type: object + description: Serializer for Mobile authenticator devices + properties: + name: + type: string + minLength: 1 + description: The human-readable name of this device. + maxLength: 64 + required: + - name ModelEnum: enum: - authentik_crypto.certificatekeypair @@ -33262,6 +34178,8 @@ components: - authentik_sources_saml.usersamlsourceconnection - authentik_stages_authenticator_duo.authenticatorduostage - authentik_stages_authenticator_duo.duodevice + - authentik_stages_authenticator_mobile.authenticatormobilestage + - authentik_stages_authenticator_mobile.mobiledevice - authentik_stages_authenticator_sms.authenticatorsmsstage - authentik_stages_authenticator_sms.smsdevice - authentik_stages_authenticator_static.authenticatorstaticstage @@ -33338,6 +34256,8 @@ components: * `authentik_sources_saml.usersamlsourceconnection` - User SAML Source Connection * `authentik_stages_authenticator_duo.authenticatorduostage` - Duo Authenticator Setup Stage * `authentik_stages_authenticator_duo.duodevice` - Duo Device + * `authentik_stages_authenticator_mobile.authenticatormobilestage` - Mobile Authenticator Setup Stage + * `authentik_stages_authenticator_mobile.mobiledevice` - Mobile Device * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage * `authentik_stages_authenticator_sms.smsdevice` - SMS Device * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage @@ -34397,6 +35317,41 @@ components: required: - pagination - results + PaginatedAuthenticatorMobileStageList: + type: object + properties: + pagination: + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + results: + type: array + items: + $ref: '#/components/schemas/AuthenticatorMobileStage' + required: + - pagination + - results PaginatedAuthenticatorSMSStageList: type: object properties: @@ -34781,6 +35736,41 @@ components: required: - pagination - results + PaginatedMobileDeviceList: + type: object + properties: + pagination: + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + results: + type: array + items: + $ref: '#/components/schemas/MobileDevice' + required: + - pagination + - results PaginatedNotificationList: type: object properties: @@ -35887,6 +36877,27 @@ components: admin_secret_key: type: string writeOnly: true + PatchedAuthenticatorMobileStageRequest: + type: object + description: AuthenticatorMobileStage 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 PatchedAuthenticatorSMSStageRequest: type: object description: AuthenticatorSMSStage Serializer @@ -36325,6 +37336,7 @@ components: * `authentik.sources.saml` - authentik Sources.SAML * `authentik.stages.authenticator` - authentik Stages.Authenticator * `authentik.stages.authenticator_duo` - authentik Stages.Authenticator.Duo + * `authentik.stages.authenticator_mobile` - authentik Stages.Authenticator.Mobile * `authentik.stages.authenticator_sms` - authentik Stages.Authenticator.SMS * `authentik.stages.authenticator_static` - authentik Stages.Authenticator.Static * `authentik.stages.authenticator_totp` - authentik Stages.Authenticator.TOTP @@ -36396,6 +37408,8 @@ components: * `authentik_sources_saml.usersamlsourceconnection` - User SAML Source Connection * `authentik_stages_authenticator_duo.authenticatorduostage` - Duo Authenticator Setup Stage * `authentik_stages_authenticator_duo.duodevice` - Duo Device + * `authentik_stages_authenticator_mobile.authenticatormobilestage` - Mobile Authenticator Setup Stage + * `authentik_stages_authenticator_mobile.mobiledevice` - Mobile Device * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage * `authentik_stages_authenticator_sms.smsdevice` - SMS Device * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage @@ -36906,6 +37920,15 @@ components: key: type: string minLength: 1 + PatchedMobileDeviceRequest: + type: object + description: Serializer for Mobile authenticator devices + properties: + name: + type: string + minLength: 1 + description: The human-readable name of this device. + maxLength: 64 PatchedNotificationRequest: type: object description: Notification Serializer diff --git a/web/src/admin/stages/StageListPage.ts b/web/src/admin/stages/StageListPage.ts index fb28cf42d..94099c84b 100644 --- a/web/src/admin/stages/StageListPage.ts +++ b/web/src/admin/stages/StageListPage.ts @@ -1,6 +1,7 @@ import "@goauthentik/admin/stages/StageWizard"; import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm"; import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm"; +import "@goauthentik/admin/stages/authenticator_mobile/AuthenticatorMobileStageForm"; import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm"; import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm"; import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm"; diff --git a/web/src/admin/stages/authenticator_mobile/AuthenticatorMobileStageForm.ts b/web/src/admin/stages/authenticator_mobile/AuthenticatorMobileStageForm.ts new file mode 100644 index 000000000..d7ad6be2a --- /dev/null +++ b/web/src/admin/stages/authenticator_mobile/AuthenticatorMobileStageForm.ts @@ -0,0 +1,130 @@ +import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; +import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; +import "@goauthentik/elements/forms/SearchSelect"; + +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { + AuthenticatorMobileStage, + AuthenticatorMobileStageRequest, + Flow, + FlowsApi, + FlowsInstancesListDesignationEnum, + FlowsInstancesListRequest, + StagesApi, +} from "@goauthentik/api"; + +@customElement("ak-stage-authenticator-mobile-form") +export class AuthenticatorMobileStageForm extends ModelForm { + loadInstance(pk: string): Promise { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorMobileRetrieve({ + stageUuid: pk, + }); + } + + getSuccessMessage(): string { + if (this.instance) { + return msg("Successfully updated stage."); + } else { + return msg("Successfully created stage."); + } + } + + async send(data: AuthenticatorMobileStage): Promise { + if (this.instance) { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorMobilePartialUpdate({ + stageUuid: this.instance.pk || "", + patchedAuthenticatorMobileStageRequest: data, + }); + } else { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorMobileCreate({ + authenticatorMobileStageRequest: data as unknown as AuthenticatorMobileStageRequest, + }); + } + } + + renderForm(): TemplateResult { + return html`
+
+ ${msg( + "Stage used to configure a mobile-based authenticator. This stage should be used for configuration flows.", + )} +
+ + + + + +

+ ${msg( + "Display name of this authenticator, used by users when they enroll an authenticator.", + )} +

+
+ + ${msg("Stage-specific settings")} +
+ + => { + const args: FlowsInstancesListRequest = { + ordering: "slug", + designation: + FlowsInstancesListDesignationEnum.StageConfiguration, + }; + if (query !== undefined) { + args.search = query; + } + const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList( + args, + ); + return flows.results; + }} + .renderElement=${(flow: Flow): string => { + return RenderFlowOption(flow); + }} + .renderDescription=${(flow: Flow): TemplateResult => { + return html`${flow.name}`; + }} + .value=${(flow: Flow | undefined): string | undefined => { + return flow?.pk; + }} + .selected=${(flow: Flow): boolean => { + return this.instance?.configureFlow === flow.pk; + }} + ?blankable=${true} + > + +

+ ${msg( + "Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.", + )} +

+
+
+
+
`; + } +} diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index bc1dec3a8..8cbb9541a 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -337,6 +337,12 @@ export class FlowExecutor extends Interface implements StageHost { .host=${this as StageHost} .challenge=${this.challenge} >`; + case "ak-stage-authenticator-mobile": + await import("@goauthentik/flow/stages/authenticator_mobile/AuthenticatorMobileStage"); + return html``; case "ak-stage-authenticator-static": await import( "@goauthentik/flow/stages/authenticator_static/AuthenticatorStaticStage" diff --git a/web/src/flow/stages/authenticator_mobile/AuthenticatorMobileStage.ts b/web/src/flow/stages/authenticator_mobile/AuthenticatorMobileStage.ts new file mode 100644 index 000000000..0717ff265 --- /dev/null +++ b/web/src/flow/stages/authenticator_mobile/AuthenticatorMobileStage.ts @@ -0,0 +1,81 @@ +import "@goauthentik/elements/EmptyState"; +import "@goauthentik/elements/forms/FormElement"; +import "@goauthentik/flow/FormStatic"; +import { BaseStage } from "@goauthentik/flow/stages/base"; + +import { msg } from "@lit/localize"; +import { CSSResult, TemplateResult, css, html } from "lit"; +import { customElement } 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"; +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 { + AuthenticatorMobileChallenge, + AuthenticatorMobileChallengeResponseRequest, +} from "@goauthentik/api"; + +@customElement("ak-stage-authenticator-mobile") +export class AuthenticatorMobileStage extends BaseStage< + AuthenticatorMobileChallenge, + AuthenticatorMobileChallengeResponseRequest +> { + static get styles(): CSSResult[] { + return [ + PFBase, + PFLogin, + PFForm, + PFFormControl, + PFTitle, + PFButton, + css` + .qr-container { + display: flex; + flex-direction: column; + place-items: center; + } + `, + ]; + } + + render(): TemplateResult { + if (!this.challenge) { + return html` + `; + } + return html` + + `; + } +}