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:

committed by
GitHub

parent
a8fd0c376f
commit
4ba360e7af
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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`
|
||||
|
@ -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":
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}
|
||||
|
@ -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";
|
||||
|
@ -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),
|
||||
|
@ -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":
|
||||
|
Reference in New Issue
Block a user