stages/identification: add captcha to identification stage (#11711)
* add captcha to identification stage * simplify component invocations * fail fast on `onTokenChange` default behavior * reword docs * rename `token` to `captcha_token` in Identification stage contexts (In Captcha stage contexts the name `token` seems well-scoped.) * use `nothing` instead of ``` html`` ``` * remove rendered Captcha component from document flow on Identification stages Note: this doesn't remove the captcha itself, if interactive, only the loading indicator. * add invisible requirement to captcha on Identification stage * stylize docs * add friendlier error messages to Captcha stage * fix tests * make captcha error messages even friendlier * add test case to retriable captcha * use default Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -21,6 +21,7 @@ import {
|
||||
SourcesApi,
|
||||
Stage,
|
||||
StagesApi,
|
||||
StagesCaptchaListRequest,
|
||||
StagesPasswordListRequest,
|
||||
UserFieldsEnum,
|
||||
} from "@goauthentik/api";
|
||||
@ -140,19 +141,13 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
|
||||
).stagesPasswordList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
.selected=${(stage: Stage): boolean => {
|
||||
return stage.pk === this.instance?.passwordStage;
|
||||
}}
|
||||
?blankable=${true}
|
||||
.groupBy=${(items: Stage[]) =>
|
||||
groupBy(items, (stage) => stage.verboseNamePlural)}
|
||||
.renderElement=${(stage: Stage): string => stage.name}
|
||||
.value=${(stage: Stage | undefined): string | undefined => stage?.pk}
|
||||
.selected=${(stage: Stage): boolean =>
|
||||
stage.pk === this.instance?.passwordStage}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
@ -161,6 +156,35 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Captcha stage")} name="captchaStage">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesCaptchaListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(
|
||||
DEFAULT_CONFIG,
|
||||
).stagesCaptchaList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) =>
|
||||
groupBy(items, (stage) => stage.verboseNamePlural)}
|
||||
.renderElement=${(stage: Stage): string => stage.name}
|
||||
.value=${(stage: Stage | undefined): string | undefined => stage?.pk}
|
||||
.selected=${(stage: Stage): boolean =>
|
||||
stage.pk === this.instance?.captchaStage}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="caseInsensitiveMatching">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
||||
@ -6,8 +6,8 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { CSSResult, PropertyValues, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@ -22,6 +22,7 @@ import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/
|
||||
interface TurnstileWindow extends Window {
|
||||
turnstile: TurnstileObject;
|
||||
}
|
||||
type TokenHandler = (token: string) => void;
|
||||
|
||||
const captchaContainerID = "captcha-container";
|
||||
|
||||
@ -45,6 +46,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
@state()
|
||||
scriptElement?: HTMLScriptElement;
|
||||
|
||||
@property()
|
||||
onTokenChange: TokenHandler = (token: string) => {
|
||||
this.host.submit({ component: "ak-stage-captcha", token });
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.captchaContainer = document.createElement("div");
|
||||
@ -102,11 +108,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
grecaptcha.ready(() => {
|
||||
const captchaId = grecaptcha.render(this.captchaContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: (token) => {
|
||||
this.host?.submit({
|
||||
token: token,
|
||||
});
|
||||
},
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
});
|
||||
grecaptcha.execute(captchaId);
|
||||
@ -122,12 +124,8 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
const captchaId = hcaptcha.render(this.captchaContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
callback: (token) => {
|
||||
this.host?.submit({
|
||||
token: token,
|
||||
});
|
||||
},
|
||||
});
|
||||
hcaptcha.execute(captchaId);
|
||||
return true;
|
||||
@ -141,16 +139,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
(window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: (token) => {
|
||||
this.host?.submit({
|
||||
token: token,
|
||||
});
|
||||
},
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
renderBody(): TemplateResult {
|
||||
renderBody() {
|
||||
if (this.error) {
|
||||
return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
|
||||
}
|
||||
@ -160,7 +154,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
render() {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state loading> </ak-empty-state>`;
|
||||
}
|
||||
|
||||
@ -4,10 +4,11 @@ import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import "@goauthentik/flow/components/ak-flow-password-input.js";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import "@goauthentik/flow/stages/captcha/CaptchaStage";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@ -46,6 +47,9 @@ export class IdentificationStage extends BaseStage<
|
||||
> {
|
||||
form?: HTMLFormElement;
|
||||
|
||||
@state()
|
||||
captchaToken = "";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
@ -274,6 +278,18 @@ export class IdentificationStage extends BaseStage<
|
||||
`
|
||||
: nothing}
|
||||
${this.renderNonFieldErrors()}
|
||||
${this.challenge.captchaStage
|
||||
? html`
|
||||
<input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
|
||||
<ak-stage-captcha
|
||||
style="visibility: hidden; position:absolute;"
|
||||
.challenge=${this.challenge.captchaStage}
|
||||
.onTokenChange=${(token: string) => {
|
||||
this.captchaToken = token;
|
||||
}}
|
||||
></ak-stage-captcha>
|
||||
`
|
||||
: nothing}
|
||||
<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}
|
||||
|
||||
Reference in New Issue
Block a user