stages/identification: allow setting of a password stage to check password and identity in a single step

closes #970

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer
2021-06-05 14:02:15 +02:00
parent f996f9d4e3
commit 24da24b5d5
16 changed files with 260 additions and 49 deletions

View File

@ -1,10 +0,0 @@
import { ChallengeChoices } from "authentik-api";
export interface Error {
code: string;
string: string;
}
export interface ErrorDict {
[key: string]: Error[];
}

View File

@ -1,8 +1,8 @@
import { customElement, LitElement, CSSResult, property, css } from "lit-element";
import { TemplateResult, html } from "lit-html";
import { Error } from "../../api/Flows";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import { ErrorDetail } from "authentik-api";
@customElement("ak-form-element")
export class FormElement extends LitElement {
@ -25,7 +25,7 @@ export class FormElement extends LitElement {
required = false;
@property({ attribute: false })
errors?: Error[];
errors?: ErrorDetail[];
updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {

View File

@ -1,4 +1,5 @@
import { LitElement, property } from "lit-element";
import { ErrorDetail } from "authentik-api";
import { html, LitElement, property, TemplateResult } from "lit-element";
export interface StageHost {
challenge?: unknown;
@ -22,4 +23,23 @@ export class BaseStage<Tin, Tout> extends LitElement {
this.host?.submit(object as unknown as Tout);
}
renderNonFieldErrors(errors: ErrorDetail[]): TemplateResult {
if (!errors) {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
</div>`;
})}
</div>`;
}
}

View File

@ -7,6 +7,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
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 PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import AKGlobal from "../../../authentik.css";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
@ -25,7 +26,7 @@ export const PasswordManagerPrefill: {
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
css`
/* login page's icons */
.pf-c-login__main-footer-links-item button {
@ -160,7 +161,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
label=${label}
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["uid_field"]}>
.errors=${(this.challenge.responseErrors || {})["uid_field"]}>
<!-- @ts-ignore -->
<input type=${type}
name="uidField"
@ -170,6 +171,25 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
class="pf-c-form-control"
required>
</ak-form-element>
${this.challenge.passwordFields ? html`
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}>
<input type="password"
name="password"
placeholder="${t`Password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}>
</ak-form-element>
`: html``}
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
html``}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primaryAction}

View File

@ -13,7 +13,6 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../../elements/Divider";
import { Error } from "../../../api/Flows";
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
@ -103,24 +102,6 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
return "";
}
renderNonFieldErrors(errors: Error[]): TemplateResult {
if (!errors) {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
</div>`;
})}
</div>`;
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state

View File

@ -2032,6 +2032,7 @@ msgstr "Loading"
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/password/PasswordStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts
@ -2549,6 +2550,8 @@ msgstr "Pass policy?"
msgid "Passing"
msgstr "Passing"
#: src/flows/stages/identification/IdentificationStage.ts
#: src/flows/stages/identification/IdentificationStage.ts
#: src/flows/stages/password/PasswordStage.ts
msgid "Password"
msgstr "Password"
@ -2558,6 +2561,10 @@ msgstr "Password"
msgid "Password field"
msgstr "Password field"
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Password stage"
msgstr "Password stage"
#: src/pages/stages/prompt/PromptForm.ts
msgid "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical."
msgstr "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical."
@ -4354,6 +4361,10 @@ msgstr "When enabled, the invitation will be deleted after usage."
msgid "When enabled, user fields are matched regardless of their casing."
msgstr "When enabled, user fields are matched regardless of their casing."
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
msgstr "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
#: src/pages/providers/saml/SAMLProviderForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."

View File

@ -2031,6 +2031,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Loading..."
msgstr ""
@ -2541,6 +2542,8 @@ msgstr ""
msgid "Passing"
msgstr ""
#:
#:
#:
msgid "Password"
msgstr ""
@ -2550,6 +2553,10 @@ msgstr ""
msgid "Password field"
msgstr ""
#:
msgid "Password stage"
msgstr ""
#:
msgid "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical."
msgstr ""
@ -4342,6 +4349,10 @@ msgstr ""
msgid "When enabled, user fields are matched regardless of their casing."
msgstr ""
#:
msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
msgstr ""
#:
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr ""

View File

@ -76,6 +76,22 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
<p class="pf-c-form__helper-text">${t`Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.`}</p>
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Password stage`}
name="passwordStage">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.passwordStage === undefined}>---------</option>
${until(new StagesApi(DEFAULT_CONFIG).stagesPasswordList({
ordering: "pk",
}).then(stages => {
return stages.results.map(stage => {
const selected = this.instance?.passwordStage === stage.pk;
return html`<option value=${ifDefined(stage.pk)} ?selected=${selected}>${stage.name}</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">${t`When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="caseInsensitiveMatching">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.caseInsensitiveMatching, true)}>