stages/authenticator_duo: initial duo stage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer
2021-05-23 21:04:37 +02:00
parent a5cd9fa141
commit 9f5a3c396d
21 changed files with 1134 additions and 25 deletions

View File

@ -13,6 +13,7 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html";
import "./access_denied/FlowAccessDenied";
import "./stages/authenticator_static/AuthenticatorStaticStage";
import "./stages/authenticator_totp/AuthenticatorTOTPStage";
import "./stages/authenticator_duo/AuthenticatorDuoStage";
import "./stages/authenticator_validate/AuthenticatorValidateStage";
import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
import "./stages/autosubmit/AutosubmitStage";
@ -46,6 +47,7 @@ import { PFSize } from "../elements/Spinner";
import { TITLE_DEFAULT } from "../constants";
import { configureSentry } from "../api/Sentry";
import { PlexAuthenticationChallenge } from "./sources/plex/PlexLoginInit";
import { AuthenticatorDuoChallenge } from "./stages/authenticator_duo/AuthenticatorDuoStage";
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost {
@ -219,6 +221,8 @@ export class FlowExecutor extends LitElement implements StageHost {
return html`<ak-stage-prompt .host=${this} .challenge=${this.challenge as PromptChallenge}></ak-stage-prompt>`;
case "ak-stage-authenticator-totp":
return html`<ak-stage-authenticator-totp .host=${this} .challenge=${this.challenge as AuthenticatorTOTPChallenge}></ak-stage-authenticator-totp>`;
case "ak-stage-authenticator-duo":
return html`<ak-stage-authenticator-duo .host=${this} .challenge=${this.challenge as AuthenticatorDuoChallenge}></ak-stage-authenticator-duo>`;
case "ak-stage-authenticator-static":
return html`<ak-stage-authenticator-static .host=${this} .challenge=${this.challenge as AuthenticatorStaticChallenge}></ak-stage-authenticator-static>`;
case "ak-stage-authenticator-webauthn":

View File

@ -0,0 +1,90 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
import { StagesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
export interface AuthenticatorDuoChallenge extends WithUserInfoChallenge {
activation_barcode: string;
activation_code: string;
stage_uuid: string;
}
@customElement("ak-stage-authenticator-duo")
export class AuthenticatorDuoStage extends BaseStage {
@property({ attribute: false })
challenge?: AuthenticatorDuoChallenge;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
firstUpdated(): void {
const i = setInterval(() => {
new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stage_uuid || "",
}).then(r => {
console.log("success");
clearInterval(i);
this.host?.submit(new FormData());
}).catch(e => {
console.log("error");
});
}, 500);
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.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.pending_user_avatar}"
user=${this.challenge.pending_user}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
</ak-form-static>
<img src=${this.challenge.activation_barcode} />
<p>
${t`Alternatively, if your current device has Duo installed, click on this link:`}
</p>
<a href=${this.challenge.activation_code}>${t`Duo activation`}</a>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>`;
}
}

View File

@ -15,6 +15,7 @@ import { Stage, StagesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import "./authenticator_duo/AuthenticatorDuoStageForm.ts";
import "./authenticator_static/AuthenticatorStaticStageForm.ts";
import "./authenticator_totp/AuthenticatorTOTPStageForm.ts";
import "./authenticator_validate/AuthenticatorValidateStageForm.ts";

View File

@ -0,0 +1,105 @@
import { FlowsApi, AuthenticatorDuoStage, StagesApi, FlowsInstancesListDesignationEnum, AuthenticatorDuoStageRequest } from "authentik-api";
import { t } from "@lingui/macro";
import { customElement } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import "../../../elements/forms/HorizontalFormElement";
import "../../../elements/forms/FormGroup";
import { until } from "lit-html/directives/until";
import { first } from "../../../utils";
import { ModelForm } from "../../../elements/forms/ModelForm";
@customElement("ak-stage-authenticator-duo-form")
export class AuthenticatorDuoStageForm extends ModelForm<AuthenticatorDuoStage, string> {
loadInstance(pk: string): Promise<AuthenticatorDuoStage> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoRetrieve({
stageUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated stage.`;
} else {
return t`Successfully created stage.`;
}
}
send = (data: AuthenticatorDuoStage): Promise<AuthenticatorDuoStage> => {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoPartialUpdate({
stageUuid: this.instance.pk || "",
patchedAuthenticatorDuoStageRequest: data
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoCreate({
authenticatorDuoStageRequest: data as unknown as AuthenticatorDuoStageRequest
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<div class="form-help-text">
${t`Stage used to configure a duo-based authenticator. This stage should be used for configuration flows.`}
</div>
<ak-form-element-horizontal
label=${t`Name`}
?required=${true}
name="name">
<input type="text" value="${ifDefined(this.instance?.name || "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header">
${t`Stage-specific settings`}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Client ID`}
?required=${true}
name="clientId">
<input type="text" value="${first(this.instance?.clientId, "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Client Secret`}
?required=${true}
?writeOnly=${this.instance !== undefined}
name="clientSecret">
<input type="text" value="" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`API Hostname`}
?required=${true}
name="apiHostname">
<input type="text" value="${first(this.instance?.apiHostname, "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Configuration flow`}
name="configureFlow">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.configureFlow === undefined}>---------</option>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.StageConfiguration,
}).then(flows => {
return flows.results.map(flow => {
let selected = this.instance?.configureFlow === flow.pk;
if (!this.instance?.pk && !this.instance?.configureFlow && flow.slug === "default-otp-time-configure") {
selected = true;
}
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">
${t`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>`;
}
}

View File

@ -34,8 +34,8 @@ export class AuthenticatorStaticStageForm extends ModelForm<AuthenticatorStaticS
authenticatorStaticStageRequest: data
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesUserWriteCreate({
userWriteStageRequest: data
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorStaticCreate({
authenticatorStaticStageRequest: data
});
}
};

View File

@ -21,6 +21,7 @@ import "../../elements/Tabs";
import "../../elements/PageHeader";
import "./tokens/UserTokenList";
import "./UserDetailsPage";
import "./settings/UserSettingsAuthenticatorDuo";
import "./settings/UserSettingsAuthenticatorStatic";
import "./settings/UserSettingsAuthenticatorTOTP";
import "./settings/UserSettingsAuthenticatorWebAuthn";
@ -48,6 +49,9 @@ export class UserSettingsPage extends LitElement {
case "ak-user-settings-authenticator-static":
return html`<ak-user-settings-authenticator-static objectId=${stage.objectUid}>
</ak-user-settings-authenticator-static>`;
case "ak-user-settings-authenticator-duo":
return html`<ak-user-settings-authenticator-duo objectId=${stage.objectUid}>
</ak-user-settings-authenticator-duo>`;
default:
return html`<p>${t`Error: unsupported stage settings: ${stage.component}`}</p>`;
}

View File

@ -0,0 +1,79 @@
import { AuthenticatorsApi } from "authentik-api";
import { t } from "@lingui/macro";
import { customElement, html, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { FlowURLManager } from "../../../api/legacy";
import { BaseUserSettings } from "./BaseUserSettings";
@customElement("ak-user-settings-authenticator-duo")
export class UserSettingsAuthenticatorDuo extends BaseUserSettings {
@property({ type: Boolean })
configureFlow = false;
renderEnabled(): TemplateResult {
return html`<div class="pf-c-card__body">
<p>
${t`Status: Enabled`}
<i class="pf-icon pf-icon-ok"></i>
</p>
<ul class="ak-otp-tokens">
${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => {
if (devices.results.length < 1) {
return;
}
return devices.results[0].tokenSet?.map((token) => {
return html`<li>${token.token}</li>`;
});
}))}
</ul>
</div>
<div class="pf-c-card__footer">
<button
class="pf-c-button pf-m-danger"
@click=${() => {
return new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => {
if (devices.results.length < 1) {
return;
}
// TODO: Handle multiple devices, currently we assume only one TOTP Device
return new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticDestroy({
id: devices.results[0].pk || 0
});
});
}}>
${t`Disable Static Tokens`}
</button>
</div>`;
}
renderDisabled(): TemplateResult {
return html`
<div class="pf-c-card__body">
<p>
${t`Status: Disabled`}
<i class="pf-icon pf-icon-error-circle-o"></i>
</p>
</div>
<div class="pf-c-card__footer">
${this.configureFlow ?
html`<a href="${FlowURLManager.configure(this.objectId || "", "?next=/%23%2Fuser")}"
class="pf-c-button pf-m-primary">${t`Enable Static Tokens`}
</a>`: html``}
</div>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">
${t`Duo`}
</div>
${this.renderDisabled()}
${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => {
return devices.results.length > 0 ? this.renderEnabled() : this.renderDisabled();
}))}
</div>`;
}
}

View File

@ -72,7 +72,7 @@ export class UserSettingsAuthenticatorStatic extends BaseUserSettings {
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">
${t`Time-based One-Time Passwords`}
${t`Static tokens`}
</div>
${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => {
return devices.results.length > 0 ? this.renderEnabled() : this.renderDisabled();

View File

@ -57,7 +57,7 @@ export class UserSettingsAuthenticatorTOTP extends BaseUserSettings {
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__title">
${t`Static tokens`}
${t`Time-based One-Time Passwords`}
</div>
${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsTotpList({}).then((devices) => {
return devices.results.length > 0 ? this.renderEnabled() : this.renderDisabled();