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.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_mobile",
|
||||
"authentik.stages.authenticator_sms",
|
||||
"authentik.stages.authenticator_static",
|
||||
"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",
|
||||
"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": {
|
||||
|
|
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/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";
|
||||
|
|
|
@ -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}
|
||||
.challenge=${this.challenge}
|
||||
></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":
|
||||
await import(
|
||||
"@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