stages/authenticator_email: Email OTP (#12630)

* stages/authenticator_email: Add basic structure for stages/authenticator_email

* stages/authenticator_email: Add stages/authenticator_email django app to settings.py

* stages/authenticator_email: Fix imports due changes introduced in #12598

* stages/authenticator_email: fix linting

* stages/authenticator_email: Add tests for token verification

* Add UI structure for authenticator_email

* Add autheticator_email to AuthenticatorValidateStageForm.ts and create AuthenticatorEmailStageForm.ts

* Add serializer property to emaildevice

* Add DeviceClasses.EMAIL to DeviceClasses

* Add migration file for DeviceClasses change (added email)

* Add new schema.yml and blueprints/schema.json to refelct email authenticator

* Fix UI to show the Email Authenticator

* Add support for email templates for the email authenticator

* Add templates

* Add DeviceClasses.EMAIL option to authenticator_validate/stage.py

* Fix logic for sending emails in stage.py and use the proper class AuthenticatorEmailStage in tasks.py

* Fix token expiration display in the email templates

* Fix authenticator email stage set up

* Add template and email to api response for Authenticator Email stage

* Fix  Authenticator Email stage set up form

* Use different flow if the user has an email configured or not for Authenticator Email stage UI

* Use the correct field for the token in AuthenticatorEmailStage.ts

* Fix linting and code style

* Use the correct assertions in tests

* Fix mask email helper

* Add missing cases for Email Authenticator in the UI

* Fix email sending, add _compose_email() method to EmailDevice

* Fix cosmetic changes

* Add support for email device challenge validation in validate_selected_challenge

* Fix tests

* Add from_address to email template

* Refactor tests

* Update API Schema

* Refactor AuthenticatorEmailStage UI for cleaner code

* Fix saving token_expiry in the stage configuration

* Remove debug statements

* Add email connection settings to the Email authenticator stage configuration UI

* Remove unused field activate_on_success from AuthenticatorEmailStage

* Add tests for duplicate email, token expiration and template error

* cosmetic/styling changes

* Use authentik's GroupMemberSerializer and ManagedAppConfig in api and apps for email authenticathor

* stages/authenticator_email: Fix typos, styling and unused fields

* stages/authenticator_email: remove unused field responseStatus

* stages/authenticator_email: regen migrations

* Fix linting issues

* Fix app label issue, typos, missing user field

* Add a trailing space in email_otp.txt RFC 3676 sec. 4.3

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* Move mask_email method to a helper function in authentik.lib.utils.email

* Remove unused function

* Use authentik.stages.email.tasks instead of authentik.stages.authenticator_email.tasks, delete authentik.stages.authenticator_email.tasks

* Fix use global settings not using the global setting if there's a default

* Revert "Fix use global settings not using the global setting if there's a default"

This reverts commit 3825248bb4.

* Use user email from user attributes if exists

* Show masked email in AuthenticatorValidateStageCode

* Remove unused base.html template

* Fix linting issues

* Change token_expiry from integer to TextField, use timedelta_string_validator where necessary to process the change

* Move 'use global connection settings' up in the Email Authenticator Stage Configuration

* Show expanded connections settings when 'use global settings' is not activated for better UX

* Fix migration file, add missing validator

* Fix test for no prefilled email address

* Add tests to check session management, challenge generation and challenge response validation

* fix linting

* Add default value EmailStage for stage_class in stage.email.tasks.send_mail

* Change string representation for EmailDevice to handle authentik/events/tests/test_models.py::TestModels, add tests for the new __str__ method

* Add #nosec to skip false positive in linting validation

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* Change Email Authenticator Setup Stage name for consistency with other authenticators

* Add tests to test properties and methods of EmailDevice and AuthenticatorEmailStage, add test for email tasks

* Add tests for email challenge in authenticator_validate

* Update migration to reflect new verbose name for AuthenticatorEmailStage

* Update schema.yml to reflect new verbose name for AuthenticatorEmailStage

* Add default email subject in Email Authenticator Setup Stage configuration

* Remove from_address from email template to ensure global settings use if use global settings is on

* Add flow-default-authenticator-email-setup.yaml blueprint

* Move email authenticator blueprint to the examples folder

* Update authentik/stages/authenticator_email/models.py

Signed-off-by: Jens L. <jens@beryju.org>

* Change self.user_pk to self.user_id because user_pk doesn't exists here

* Remove unused logger import

* Remove more unused logger import

* Add error handling to authentik.lib.utils.email.mask_email

* fix linting

* don't catch Exception

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update icons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Marcelo Elizeche Landó
2025-02-17 11:16:58 -03:00
committed by GitHub
parent a8fd0c376f
commit 4ba360e7af
33 changed files with 3286 additions and 18 deletions

View File

@ -2,6 +2,7 @@ import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/stages/StageWizard";
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm";
import "@goauthentik/admin/stages/authenticator_email/AuthenticatorEmailStageForm";
import "@goauthentik/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm";
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";

View File

@ -1,6 +1,7 @@
import "@goauthentik/admin/common/ak-license-notice";
import { StageBindingForm } from "@goauthentik/admin/flows/StageBindingForm";
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
import "@goauthentik/admin/stages/authenticator_email/AuthenticatorEmailStageForm";
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";

View File

@ -0,0 +1,283 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
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 "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
AuthenticatorEmailStage,
Flow,
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
StagesApi,
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-email-form")
export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmailStage> {
async loadInstance(pk: string): Promise<AuthenticatorEmailStage> {
const stage = await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEmailRetrieve({
stageUuid: pk,
});
this.showConnectionSettings = !stage.useGlobalSettings;
return stage;
}
@property({ type: Boolean })
showConnectionSettings = false;
async send(data: AuthenticatorEmailStage): Promise<AuthenticatorEmailStage> {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEmailUpdate({
stageUuid: this.instance.pk || "",
authenticatorEmailStageRequest: data,
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEmailCreate({
authenticatorEmailStageRequest: data,
});
}
}
renderConnectionSettings(): TemplateResult {
if (!this.showConnectionSettings) {
return html``;
}
return html`<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("SMTP Host")} ?required=${true} name="host">
<input
type="text"
value="${ifDefined(this.instance?.host || "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("SMTP Port")} ?required=${true} name="port">
<input
type="number"
value="${first(this.instance?.port, 25)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("SMTP Username")} name="username">
<input
type="text"
value="${ifDefined(this.instance?.username || "")}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("SMTP Password")}
?writeOnly=${this.instance !== undefined}
name="password"
>
<input type="text" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
<ak-form-element-horizontal name="useTls">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useTls, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Use TLS")}</span>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="useSsl">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useSsl, false)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Use SSL")}</span>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Timeout")}
?required=${true}
name="timeout"
>
<input
type="number"
value="${first(this.instance?.timeout, 30)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("From address")}
?required=${true}
name="fromAddress"
>
<input
type="text"
value="${ifDefined(this.instance?.fromAddress || "system@authentik.local")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Email address the verification email will be sent from.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}
renderForm(): TemplateResult {
return html` <span> ${msg("Stage used to configure an email-based authenticator.")} </span>
<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-element-horizontal name="useGlobalSettings">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useGlobalSettings, true)}
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.showConnectionSettings = !target.checked;
}}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Use global connection settings")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, global email connection settings will be used and connection settings below will be ignored.",
)}
</p>
</ak-form-element-horizontal>
${this.renderConnectionSettings()}
<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("Subject")}
?required=${true}
name="subject"
>
<input
type="text"
value="${first(this.instance?.subject, "authentik Sign-in code")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Subject of the verification email.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Token expiration")}
?required=${true}
name="tokenExpiry"
>
<input
type="text"
value="${first(this.instance?.tokenExpiry, "minutes=15")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).",
)}
</p>
</ak-form-element-horizontal>
<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>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-stage-authenticator-email-form": AuthenticatorEmailStageForm;
}
}

View File

@ -79,6 +79,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
[DeviceClassesEnum.Webauthn, msg("WebAuthn Authenticators")],
[DeviceClassesEnum.Duo, msg("Duo Authenticators")],
[DeviceClassesEnum.Sms, msg("SMS-based Authenticators")],
[DeviceClassesEnum.Email, msg("Email-based Authenticators")],
];
return html`

View File

@ -58,6 +58,8 @@ export class UserDeviceTable extends Table<Device> {
switch (device.type) {
case "authentik_stages_authenticator_duo.DuoDevice":
return api.authenticatorsAdminDuoDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_email.EmailDevice":
return api.authenticatorsAdminEmailDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_sms.SMSDevice":
return api.authenticatorsAdminSmsDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_totp.TOTPDevice":

View File

@ -392,6 +392,14 @@ export class FlowExecutor extends Interface implements StageHost {
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-webauthn>`;
case "ak-stage-authenticator-email":
await import(
"@goauthentik/flow/stages/authenticator_email/AuthenticatorEmailStage"
);
return html`<ak-stage-authenticator-email
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-email>`;
case "ak-stage-authenticator-sms":
await import("@goauthentik/flow/stages/authenticator_sms/AuthenticatorSMSStage");
return html`<ak-stage-authenticator-sms

View File

@ -0,0 +1,173 @@
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, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
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 {
AuthenticatorEmailChallenge,
AuthenticatorEmailChallengeResponseRequest,
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-email")
export class AuthenticatorEmailStage extends BaseStage<
AuthenticatorEmailChallenge,
AuthenticatorEmailChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
}
renderEmailInput(): TemplateResult {
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>
<ak-form-element
label="${msg("Configure your email")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["email"]}
>
<input
type="email"
name="email"
placeholder="${msg("Please enter your email address.")}"
autofocus=""
autocomplete="email"
class="pf-c-form-control"
required
/>
</ak-form-element>
${this.renderNonFieldErrors()}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
renderEmailOTPInput(): TemplateResult {
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">
<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>
A verification token has been sent to your configured email address
${ifDefined(this.challenge.email)}
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-element
label="${msg("Code")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<input
type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
placeholder="${msg("Please enter the code you received via email")}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
required
/>
</ak-form-element>
${this.renderNonFieldErrors()}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
render(): TemplateResult {
console.debug(
"authentik/stages/authenticator_email:",
this.challenge ? this.challenge.emailRequired : undefined,
);
if (!this.challenge) {
console.debug(
"authentik/stages/authenticator_email: AuthenticatorEmailStage.render() called without challenge",
);
return html`<ak-empty-state loading> </ak-empty-state>`;
}
if (this.challenge.emailRequired) {
console.debug(
"authentik/stages/authenticator_email: AuthenticatorEmailStage.render() called with challenge",
this.challenge,
);
return this.renderEmailInput();
}
console.debug(
"authentik/stages/authenticator_email: AuthenticatorEmailStage.render() called without emailRequired challenge",
this.challenge,
);
return this.renderEmailOTPInput();
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-stage-authenticator-email": AuthenticatorEmailStage;
}
}

View File

@ -185,6 +185,12 @@ export class AuthenticatorValidateStage
<p>${msg("SMS")}</p>
<small>${msg("Tokens sent via SMS.")}</small>
</div>`;
case DeviceClassesEnum.Email:
return html`<i class="fas fa-envelope-o"></i>
<div class="right">
<p>${msg("Email")}</p>
<small>${msg("Tokens sent via email.")}</small>
</div>`;
default:
break;
}
@ -240,6 +246,7 @@ export class AuthenticatorValidateStage
switch (this.selectedDeviceChallenge?.deviceClass) {
case DeviceClassesEnum.Static:
case DeviceClassesEnum.Totp:
case DeviceClassesEnum.Email:
case DeviceClassesEnum.Sms:
return html` <ak-stage-authenticator-validate-code
.host=${this}

View File

@ -33,6 +33,10 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
deviceMessage(): string {
switch (this.deviceChallenge?.deviceClass) {
case DeviceClassesEnum.Email: {
const email = this.deviceChallenge.challenge?.email;
return msg(`A code has been sent to you via email${email ? ` ${email}` : ""}`);
}
case DeviceClassesEnum.Sms:
return msg("A code has been sent to you via SMS.");
case DeviceClassesEnum.Totp:
@ -48,12 +52,14 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
deviceIcon(): string {
switch (this.deviceChallenge?.deviceClass) {
case DeviceClassesEnum.Email:
return "fa-envelope-o";
case DeviceClassesEnum.Sms:
return "fa-key";
case DeviceClassesEnum.Totp:
return "fa-mobile-alt";
case DeviceClassesEnum.Totp:
return "fa-clock";
case DeviceClassesEnum.Static:
return "fa-sticky-note";
return "fa-key";
}
return "fa-mobile-alt";

View File

@ -34,6 +34,12 @@ export class MFADeviceForm extends ModelForm<Device, string> {
duoDeviceRequest: device,
});
break;
case "authentik_stages_authenticator_email.EmailDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsEmailUpdate({
id: parseInt(this.instance?.pk, 10),
emailDeviceRequest: device,
});
break;
case "authentik_stages_authenticator_sms.SMSDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsSmsUpdate({
id: parseInt(this.instance?.pk, 10),

View File

@ -95,6 +95,8 @@ export class MFADevicesPage extends Table<Device> {
switch (device.type) {
case "authentik_stages_authenticator_duo.DuoDevice":
return api.authenticatorsDuoDestroy(id);
case "authentik_stages_authenticator_email.EmailDevice":
return api.authenticatorsEmailDestroy(id);
case "authentik_stages_authenticator_sms.SMSDevice":
return api.authenticatorsSmsDestroy(id);
case "authentik_stages_authenticator_totp.TOTPDevice":