stages/captcha: Run interactive captcha in Frame (#11857)
* initial turnstile frame Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add interactive flag Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add interactive support for all Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't hide in identification stage if interactive Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * require less hacky css Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -2,6 +2,7 @@ import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
@ -80,6 +81,15 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-switch-input
|
||||
name="interactive"
|
||||
label=${msg("Interactive")}
|
||||
?checked="${this.instance?.interactive}"
|
||||
help=${msg(
|
||||
"Enable this flag if the configured captcha requires User-interaction. Required for reCAPTCHA v2, hCaptcha and Cloudflare Turnstile.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-number-input
|
||||
label=${msg("Score minimum threshold")}
|
||||
required
|
||||
|
||||
@ -10,10 +10,14 @@ export const DOM_PURIFY_STRICT: DOMPurify.Config = {
|
||||
ALLOWED_TAGS: ["#text"],
|
||||
};
|
||||
|
||||
export async function renderStatic(input: TemplateResult): Promise<string> {
|
||||
return await collectResult(render(input));
|
||||
}
|
||||
|
||||
export function purify(input: TemplateResult): TemplateResult {
|
||||
return html`${until(
|
||||
(async () => {
|
||||
const rendered = await collectResult(render(input));
|
||||
const rendered = await renderStatic(input);
|
||||
const purified = DOMPurify.sanitize(rendered);
|
||||
return html`${unsafeHTML(purified)}`;
|
||||
})(),
|
||||
|
||||
@ -107,7 +107,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
|
||||
?loading="${this.authenticating}"
|
||||
header=${this.authenticating
|
||||
? msg("Authenticating...")
|
||||
: this.errorMessage || msg("Failed to authenticate")}
|
||||
: this.errorMessage || msg("Loading")}
|
||||
icon="fa-times"
|
||||
>
|
||||
</ak-empty-state>
|
||||
|
||||
@ -10,7 +10,7 @@ import "../../../stories/flow-interface";
|
||||
import "./CaptchaStage";
|
||||
|
||||
export default {
|
||||
title: "Flow / Stages / CaptchaStage",
|
||||
title: "Flow / Stages / Captcha",
|
||||
};
|
||||
|
||||
export const LoadingNoChallenge = () => {
|
||||
@ -25,92 +25,60 @@ export const LoadingNoChallenge = () => {
|
||||
</ak-storybook-interface>`;
|
||||
};
|
||||
|
||||
export const ChallengeGoogleReCaptcha: StoryObj = {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: {
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://www.google.com/recaptcha/api.js",
|
||||
siteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
|
||||
} as CaptchaChallenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
function captchaFactory(challenge: CaptchaChallenge): StoryObj {
|
||||
return {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: challenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const ChallengeHCaptcha: StoryObj = {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: {
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://js.hcaptcha.com/1/api.js",
|
||||
siteKey: "10000000-ffff-ffff-ffff-000000000001",
|
||||
} as CaptchaChallenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export const ChallengeHCaptcha = captchaFactory({
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://js.hcaptcha.com/1/api.js",
|
||||
siteKey: "10000000-ffff-ffff-ffff-000000000001",
|
||||
interactive: true,
|
||||
} as CaptchaChallenge);
|
||||
|
||||
export const ChallengeTurnstile: StoryObj = {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: {
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "1x00000000000000000000BB",
|
||||
} as CaptchaChallenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
export const ChallengeTurnstileVisible = captchaFactory({
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "1x00000000000000000000AA",
|
||||
interactive: true,
|
||||
} as CaptchaChallenge);
|
||||
export const ChallengeTurnstileInvisible = captchaFactory({
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "1x00000000000000000000BB",
|
||||
interactive: true,
|
||||
} as CaptchaChallenge);
|
||||
export const ChallengeTurnstileForce = captchaFactory({
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "3x00000000000000000000FF",
|
||||
interactive: true,
|
||||
} as CaptchaChallenge);
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
///<reference types="@hcaptcha/types"/>
|
||||
import { renderStatic } from "@goauthentik/common/purify";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import { randomId } from "@goauthentik/elements/utils/randomId";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, html } from "lit";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, 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";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
@ -24,12 +25,22 @@ interface TurnstileWindow extends Window {
|
||||
}
|
||||
type TokenHandler = (token: string) => void;
|
||||
|
||||
const captchaContainerID = "captcha-container";
|
||||
|
||||
@customElement("ak-stage-captcha")
|
||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
||||
return [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
css`
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 73px; /* tmp */
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
handlers = [this.handleGReCaptcha, this.handleHCaptcha, this.handleTurnstile];
|
||||
@ -38,14 +49,17 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
error?: string;
|
||||
|
||||
@state()
|
||||
captchaInteractive: boolean = true;
|
||||
captchaFrame: HTMLIFrameElement;
|
||||
|
||||
@state()
|
||||
captchaContainer: HTMLDivElement;
|
||||
captchaDocumentContainer: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
scriptElement?: HTMLScriptElement;
|
||||
|
||||
@property({ type: Boolean })
|
||||
embedded = false;
|
||||
|
||||
@property()
|
||||
onTokenChange: TokenHandler = (token: string) => {
|
||||
this.host.submit({ component: "ak-stage-captcha", token });
|
||||
@ -53,8 +67,70 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.captchaContainer = document.createElement("div");
|
||||
this.captchaContainer.id = captchaContainerID;
|
||||
this.captchaFrame = document.createElement("iframe");
|
||||
this.captchaFrame.src = "about:blank";
|
||||
this.captchaFrame.id = `ak-captcha-${randomId()}`;
|
||||
|
||||
this.captchaDocumentContainer = document.createElement("div");
|
||||
this.captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
|
||||
this.messageCallback = this.messageCallback.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("message", this.messageCallback);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("message", this.messageCallback);
|
||||
if (!this.challenge.interactive) {
|
||||
document.removeChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
messageCallback(
|
||||
ev: MessageEvent<{
|
||||
source?: string;
|
||||
context?: string;
|
||||
message: string;
|
||||
token: string;
|
||||
}>,
|
||||
) {
|
||||
const msg = ev.data;
|
||||
if (msg.source !== "goauthentik.io" || msg.context !== "flow-executor") {
|
||||
return;
|
||||
}
|
||||
if (msg.message !== "captcha") {
|
||||
return;
|
||||
}
|
||||
this.onTokenChange(msg.token);
|
||||
}
|
||||
|
||||
async renderFrame(captchaElement: TemplateResult) {
|
||||
this.captchaFrame.contentWindow?.document.open();
|
||||
this.captchaFrame.contentWindow?.document.write(
|
||||
await renderStatic(
|
||||
html`<!doctype html>
|
||||
<html>
|
||||
<body style="display:flex;flex-direction:row;justify-content:center;">
|
||||
${captchaElement}
|
||||
<script src=${this.challenge.jsUrl}></script>
|
||||
<script>
|
||||
function callback(token) {
|
||||
window.parent.postMessage({
|
||||
message: "captcha",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
token: token,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
),
|
||||
);
|
||||
this.captchaFrame.contentWindow?.document.close();
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
@ -64,15 +140,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
this.scriptElement.async = true;
|
||||
this.scriptElement.defer = true;
|
||||
this.scriptElement.dataset.akCaptchaScript = "true";
|
||||
this.scriptElement.onload = () => {
|
||||
this.scriptElement.onload = async () => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
let found = false;
|
||||
let lastError = undefined;
|
||||
this.handlers.forEach((handler) => {
|
||||
this.handlers.forEach(async (handler) => {
|
||||
let handlerFound = false;
|
||||
try {
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: trying handler`);
|
||||
handlerFound = handler.apply(this);
|
||||
handlerFound = await handler.apply(this);
|
||||
if (handlerFound) {
|
||||
console.debug(
|
||||
`authentik/stages/captcha[${handler.name}]: handler succeeded`,
|
||||
@ -96,51 +172,79 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
.querySelectorAll("[data-ak-captcha-script=true]")
|
||||
.forEach((el) => el.remove());
|
||||
document.head.appendChild(this.scriptElement);
|
||||
if (!this.challenge.interactive) {
|
||||
document.appendChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleGReCaptcha(): boolean {
|
||||
async handleGReCaptcha(): Promise<boolean> {
|
||||
if (!Object.hasOwn(window, "grecaptcha")) {
|
||||
return false;
|
||||
}
|
||||
this.captchaInteractive = false;
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
grecaptcha.ready(() => {
|
||||
const captchaId = grecaptcha.render(this.captchaContainer, {
|
||||
if (this.challenge.interactive) {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="g-recaptcha"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
} else {
|
||||
grecaptcha.ready(() => {
|
||||
const captchaId = grecaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
});
|
||||
grecaptcha.execute(captchaId);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleHCaptcha(): Promise<boolean> {
|
||||
if (!Object.hasOwn(window, "hcaptcha")) {
|
||||
return false;
|
||||
}
|
||||
if (this.challenge.interactive) {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="h-captcha"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-theme="${this.activeTheme ? this.activeTheme : "light"}"
|
||||
data-callback="callback"
|
||||
></div> `,
|
||||
);
|
||||
} else {
|
||||
const captchaId = hcaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
});
|
||||
grecaptcha.execute(captchaId);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
handleHCaptcha(): boolean {
|
||||
if (!Object.hasOwn(window, "hcaptcha")) {
|
||||
return false;
|
||||
hcaptcha.execute(captchaId);
|
||||
}
|
||||
this.captchaInteractive = false;
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
const captchaId = hcaptcha.render(this.captchaContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
});
|
||||
hcaptcha.execute(captchaId);
|
||||
return true;
|
||||
}
|
||||
|
||||
handleTurnstile(): boolean {
|
||||
async handleTurnstile(): Promise<boolean> {
|
||||
if (!Object.hasOwn(window, "turnstile")) {
|
||||
return false;
|
||||
}
|
||||
this.captchaInteractive = false;
|
||||
document.body.appendChild(this.captchaContainer);
|
||||
(window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
if (this.challenge.interactive) {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="cf-turnstile"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
} else {
|
||||
(window as unknown as TurnstileWindow).turnstile.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -148,13 +252,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
if (this.error) {
|
||||
return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
|
||||
}
|
||||
if (this.captchaInteractive) {
|
||||
return html`${this.captchaContainer}`;
|
||||
if (this.challenge.interactive) {
|
||||
return html`${this.captchaFrame}`;
|
||||
}
|
||||
return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.embedded) {
|
||||
if (!this.challenge.interactive) {
|
||||
return html``;
|
||||
}
|
||||
return this.renderBody();
|
||||
}
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state loading> </ak-empty-state>`;
|
||||
}
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
import type { StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html } from "lit";
|
||||
|
||||
import "@patternfly/patternfly/components/Login/login.css";
|
||||
|
||||
import { FlowDesignationEnum, IdentificationChallenge, UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import "../../../stories/flow-interface";
|
||||
import "./IdentificationStage";
|
||||
|
||||
export default {
|
||||
title: "Flow / Stages / Identification",
|
||||
};
|
||||
|
||||
export const LoadingNoChallenge = () => {
|
||||
return html`<ak-storybook-interface theme=${UiThemeEnum.Dark}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-identification></ak-stage-identification>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ak-storybook-interface>`;
|
||||
};
|
||||
|
||||
function identificationFactory(challenge: IdentificationChallenge): StoryObj {
|
||||
return {
|
||||
render: ({ theme, challenge }) => {
|
||||
return html`<ak-storybook-interface theme=${theme}>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<div class="pf-c-login__main">
|
||||
<ak-stage-identification
|
||||
.challenge=${challenge}
|
||||
></ak-stage-identification>
|
||||
</div>
|
||||
</div></div
|
||||
></ak-storybook-interface>`;
|
||||
},
|
||||
args: {
|
||||
theme: "automatic",
|
||||
challenge: challenge,
|
||||
},
|
||||
argTypes: {
|
||||
theme: {
|
||||
options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ChallengeDefault = identificationFactory({
|
||||
userFields: ["username"],
|
||||
passwordFields: false,
|
||||
flowDesignation: FlowDesignationEnum.Authentication,
|
||||
primaryAction: "Login",
|
||||
showSourceLabels: false,
|
||||
// jsUrl: "https://js.hcaptcha.com/1/api.js",
|
||||
// siteKey: "10000000-ffff-ffff-ffff-000000000001",
|
||||
// interactive: true,
|
||||
});
|
||||
|
||||
// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
export const ChallengeCaptchaTurnstileVisible = identificationFactory({
|
||||
userFields: ["username"],
|
||||
passwordFields: false,
|
||||
flowDesignation: FlowDesignationEnum.Authentication,
|
||||
primaryAction: "Login",
|
||||
showSourceLabels: false,
|
||||
flowInfo: {
|
||||
layout: "stacked",
|
||||
cancelUrl: "",
|
||||
title: "Foo",
|
||||
},
|
||||
captchaStage: {
|
||||
pendingUser: "",
|
||||
pendingUserAvatar: "",
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "1x00000000000000000000AA",
|
||||
interactive: true,
|
||||
},
|
||||
});
|
||||
@ -282,11 +282,11 @@ export class IdentificationStage extends BaseStage<
|
||||
? 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;
|
||||
}}
|
||||
embedded
|
||||
></ak-stage-captcha>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
Reference in New Issue
Block a user