stages/authenticator_validate: create challenge per device, implement class switcher
This commit is contained in:
@ -230,7 +230,9 @@ select[multiple] {
|
||||
.pf-c-login__main {
|
||||
background-color: var(--ak-dark-background);
|
||||
}
|
||||
.pf-c-login__main-body {
|
||||
.pf-c-login__main-body,
|
||||
.pf-c-login__main-header,
|
||||
.pf-c-login__main-header-desc {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-login__main-footer-links-item-link > img {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { gettext } from "django";
|
||||
import { css, CSSResult, customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { WithUserInfoChallenge } from "../../../api/Flows";
|
||||
import { COMMON_STYLES } from "../../../common/styles";
|
||||
import { BaseStage, StageHost } from "../base";
|
||||
import "./AuthenticatorValidateStageWebAuthn";
|
||||
import "./AuthenticatorValidateStageCode";
|
||||
|
||||
export enum DeviceClasses {
|
||||
STATIC = "static",
|
||||
@ -32,6 +35,79 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
||||
@property({attribute: false})
|
||||
selectedDeviceChallenge?: DeviceChallenge;
|
||||
|
||||
submit(formData?: FormData): Promise<void> {
|
||||
return this.host?.submit(formData) || Promise.resolve();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return COMMON_STYLES.concat(css`
|
||||
ul > li:not(:last-child) {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.authenticator-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
i {
|
||||
font-size: 1.5rem;
|
||||
padding: 1rem 0;
|
||||
width: 5rem;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.right > * {
|
||||
height: 50%;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
renderDevicePickerSingle(deviceChallenge: DeviceChallenge): TemplateResult {
|
||||
switch (deviceChallenge.device_class) {
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<i class="fas fa-mobile-alt"></i>
|
||||
<div class="right">
|
||||
<p>${gettext("Authenticator")}</p>
|
||||
<small>${gettext("Use a security key to prove your identity.")}</small>
|
||||
</div>`;
|
||||
case DeviceClasses.TOTP:
|
||||
return html`<i class="fas fa-clock"></i>
|
||||
<div class="right">
|
||||
<p>${gettext("Traditional authenticator")}</p>
|
||||
<small>${gettext("Use a code-based authenticator.")}</small>
|
||||
</div>`;
|
||||
case DeviceClasses.STATIC:
|
||||
return html`<i class="fas fa-key"></i>
|
||||
<div class="right">
|
||||
<p>${gettext("Recovery keys")}</p>
|
||||
<small>${gettext("In case you can't access any other method.")}</small>
|
||||
</div>`;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderDevicePicker(): TemplateResult {
|
||||
return html`
|
||||
<ul>
|
||||
${this.challenge?.device_challenges.map((challenges) => {
|
||||
return html`<li>
|
||||
<button class="pf-c-button authenticator-button" type="button" @click=${() => {
|
||||
this.selectedDeviceChallenge = challenges;
|
||||
}}>
|
||||
${this.renderDevicePickerSingle(challenges)}
|
||||
</button>
|
||||
</li>`;
|
||||
})}
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
renderDeviceChallenge(): TemplateResult {
|
||||
if (!this.selectedDeviceChallenge) {
|
||||
return html``;
|
||||
@ -39,8 +115,11 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
||||
switch (this.selectedDeviceChallenge?.device_class) {
|
||||
case DeviceClasses.STATIC:
|
||||
case DeviceClasses.TOTP:
|
||||
// TODO: Create input for code
|
||||
return html``;
|
||||
return html`<ak-stage-authenticator-validate-code
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}>
|
||||
</ak-stage-authenticator-validate-code>`;
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<ak-stage-authenticator-validate-webauthn
|
||||
.host=${this}
|
||||
@ -50,20 +129,29 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
|
||||
}
|
||||
}
|
||||
|
||||
submit(formData?: FormData): Promise<void> {
|
||||
return this.host?.submit(formData) || Promise.resolve();
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-loading-state></ak-loading-state>`;
|
||||
}
|
||||
// User only has a single device class, so we don't show a picker
|
||||
if (this.challenge?.device_challenges.length === 1) {
|
||||
this.selectedDeviceChallenge = this.challenge.device_challenges[0];
|
||||
}
|
||||
if (this.selectedDeviceChallenge) {
|
||||
return this.renderDeviceChallenge();
|
||||
}
|
||||
// TODO: Create picker between challenges
|
||||
return html`ak-stage-authenticator-validate`;
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.title}
|
||||
</h1>
|
||||
${this.selectedDeviceChallenge ? "" : html`<p class="pf-c-login__main-header-desc">
|
||||
${gettext("Select an identification method.")}
|
||||
</p>`}
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
${this.selectedDeviceChallenge ? this.renderDeviceChallenge() : this.renderDevicePicker()}
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { gettext } from "django";
|
||||
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { COMMON_STYLES } from "../../../common/styles";
|
||||
import { BaseStage } from "../base";
|
||||
import { AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
|
||||
|
||||
@customElement("ak-stage-authenticator-validate-code")
|
||||
export class AuthenticatorValidateStageWebCode extends BaseStage {
|
||||
|
||||
@property({ attribute: false })
|
||||
challenge?: AuthenticatorValidateStageChallenge;
|
||||
|
||||
@property({ attribute: false })
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return COMMON_STYLES;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-loading-state></ak-loading-state>`;
|
||||
}
|
||||
return html`<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
<img class="pf-c-avatar" src="${this.challenge.pending_user_avatar}" alt="${gettext("User's avatar")}">
|
||||
${this.challenge.pending_user}
|
||||
</div>
|
||||
<div class="right">
|
||||
<a href="/flows/-/cancel/">${gettext("Not you?")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ak-form-element
|
||||
label="${gettext("Code")}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.response_errors || {})["code"]}>
|
||||
<!-- @ts-ignore -->
|
||||
<input type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder="${gettext("Please enter your TOTP Code")}"
|
||||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
required="">
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${gettext("Continue")}
|
||||
</button>
|
||||
</div>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ import "../../elements/stages/prompt/PromptStage";
|
||||
import "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage";
|
||||
import "../../elements/stages/authenticator_static/AuthenticatorStaticStage";
|
||||
import "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
|
||||
import "../../elements/stages/authenticator_validate/AuthenticatorValidateStage";
|
||||
import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows";
|
||||
import { DefaultClient } from "../../api/Client";
|
||||
import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage";
|
||||
@ -21,6 +22,7 @@ import { AutosubmitChallenge } from "../../elements/stages/autosubmit/Autosubmit
|
||||
import { PromptChallenge } from "../../elements/stages/prompt/PromptStage";
|
||||
import { AuthenticatorTOTPChallenge } from "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage";
|
||||
import { AuthenticatorStaticChallenge } from "../../elements/stages/authenticator_static/AuthenticatorStaticStage";
|
||||
import { AuthenticatorValidateStageChallenge } from "../../elements/stages/authenticator_validate/AuthenticatorValidateStage";
|
||||
import { WebAuthnAuthenticatorRegisterChallenge } from "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
import { SpinnerSize } from "../../elements/Spinner";
|
||||
@ -161,6 +163,8 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
return html`<ak-stage-authenticator-static .host=${this} .challenge=${this.challenge as AuthenticatorStaticChallenge}></ak-stage-authenticator-static>`;
|
||||
case "ak-stage-authenticator-webauthn":
|
||||
return html`<ak-stage-authenticator-webauthn .host=${this} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn>`;
|
||||
case "ak-stage-authenticator-validate":
|
||||
return html`<ak-stage-authenticator-validate .host=${this} .challenge=${this.challenge as AuthenticatorValidateStageChallenge}></ak-stage-authenticator-validate>`;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
Reference in New Issue
Block a user