stages/authenticator_validate: create challenge per device, implement class switcher

This commit is contained in:
Jens Langhammer
2021-02-23 23:43:13 +01:00
parent e8259791f0
commit 3cdb81c5ba
5 changed files with 209 additions and 34 deletions

View File

@ -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 {

View File

@ -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>`;
}
}

View File

@ -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>`;
}
}

View File

@ -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;
}