initial scaffold
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
b7532740ef
commit
6e9c3affc8
|
@ -84,6 +84,7 @@ INSTALLED_APPS = [
|
||||||
"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_sms",
|
"authentik.stages.authenticator_sms",
|
||||||
"authentik.stages.authenticator_static",
|
"authentik.stages.authenticator_static",
|
||||||
"authentik.stages.authenticator_totp",
|
"authentik.stages.authenticator_totp",
|
||||||
|
|
|
@ -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"]
|
|
@ -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
|
|
@ -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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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")
|
|
@ -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()
|
|
@ -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),
|
||||||
|
]
|
|
@ -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",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -3455,6 +3527,7 @@
|
||||||
"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_sms",
|
"authentik.stages.authenticator_sms",
|
||||||
"authentik.stages.authenticator_static",
|
"authentik.stages.authenticator_static",
|
||||||
"authentik.stages.authenticator_totp",
|
"authentik.stages.authenticator_totp",
|
||||||
|
@ -3530,6 +3603,8 @@
|
||||||
"authentik_sources_saml.usersamlsourceconnection",
|
"authentik_sources_saml.usersamlsourceconnection",
|
||||||
"authentik_stages_authenticator_duo.authenticatorduostage",
|
"authentik_stages_authenticator_duo.authenticatorduostage",
|
||||||
"authentik_stages_authenticator_duo.duodevice",
|
"authentik_stages_authenticator_duo.duodevice",
|
||||||
|
"authentik_stages_authenticator_mobile.authenticatormobilestage",
|
||||||
|
"authentik_stages_authenticator_mobile.mobiledevice",
|
||||||
"authentik_stages_authenticator_sms.authenticatorsmsstage",
|
"authentik_stages_authenticator_sms.authenticatorsmsstage",
|
||||||
"authentik_stages_authenticator_sms.smsdevice",
|
"authentik_stages_authenticator_sms.smsdevice",
|
||||||
"authentik_stages_authenticator_static.authenticatorstaticstage",
|
"authentik_stages_authenticator_static.authenticatorstaticstage",
|
||||||
|
@ -5843,6 +5918,125 @@
|
||||||
},
|
},
|
||||||
"required": []
|
"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": {
|
"model_authentik_stages_authenticator_sms.authenticatorsmsstage": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
1023
schema.yml
1023
schema.yml
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,7 @@
|
||||||
import "@goauthentik/admin/stages/StageWizard";
|
import "@goauthentik/admin/stages/StageWizard";
|
||||||
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
|
||||||
import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm";
|
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_sms/AuthenticatorSMSStageForm";
|
||||||
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
|
||||||
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
|
||||||
|
|
|
@ -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<AuthenticatorMobileStage, string> {
|
||||||
|
loadInstance(pk: string): Promise<AuthenticatorMobileStage> {
|
||||||
|
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<AuthenticatorMobileStage> {
|
||||||
|
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`<form class="pf-c-form pf-m-horizontal">
|
||||||
|
<div class="form-help-text">
|
||||||
|
${msg(
|
||||||
|
"Stage used to configure a mobile-based authenticator. This stage should be used for configuration flows.",
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.name, "")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Authenticator type name")}
|
||||||
|
?required=${false}
|
||||||
|
name="friendlyName"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.friendlyName, "")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"Display name of this authenticator, used by users when they enroll an authenticator.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-group .expanded=${true}>
|
||||||
|
<span slot="header"> ${msg("Stage-specific settings")} </span>
|
||||||
|
<div slot="body" class="pf-c-form">
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Configuration flow")}
|
||||||
|
name="configureFlow"
|
||||||
|
>
|
||||||
|
<ak-search-select
|
||||||
|
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
</ak-search-select>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
</div>
|
||||||
|
</ak-form-group>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -337,6 +337,12 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||||
.host=${this as StageHost}
|
.host=${this as StageHost}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
></ak-stage-authenticator-duo>`;
|
></ak-stage-authenticator-duo>`;
|
||||||
|
case "ak-stage-authenticator-mobile":
|
||||||
|
await import("@goauthentik/flow/stages/authenticator_mobile/AuthenticatorMobileStage");
|
||||||
|
return html`<ak-stage-authenticator-mobile
|
||||||
|
.host=${this as StageHost}
|
||||||
|
.challenge=${this.challenge}
|
||||||
|
></ak-stage-authenticator-mobile>`;
|
||||||
case "ak-stage-authenticator-static":
|
case "ak-stage-authenticator-static":
|
||||||
await import(
|
await import(
|
||||||
"@goauthentik/flow/stages/authenticator_static/AuthenticatorStaticStage"
|
"@goauthentik/flow/stages/authenticator_static/AuthenticatorStaticStage"
|
||||||
|
|
|
@ -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`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||||
|
</ak-empty-state>`;
|
||||||
|
}
|
||||||
|
return html`<header class="pf-c-login__main-header">
|
||||||
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
|
</header>
|
||||||
|
<div class="pf-c-login__main-body">
|
||||||
|
<form
|
||||||
|
class="pf-c-form"
|
||||||
|
@submit=${(e: Event) => {
|
||||||
|
this.submitForm(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ak-form-static
|
||||||
|
class="pf-c-form__group"
|
||||||
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
|
<div slot="link">
|
||||||
|
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||||
|
>${msg("Not you?")}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ak-form-static>
|
||||||
|
<div class="qr-container">
|
||||||
|
<qr-code data="${JSON.stringify(this.challenge)}"></qr-code>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="pf-c-login__main-footer">
|
||||||
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
|
</footer>`;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue