stages/captcha: Run interactive captcha in Frame (#11857) * initial turnstile frame * add interactive flag * add interactive support for all * fix missing migration * don't hide in identification stage if interactive * fixup * require less hacky css * update docs --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							f6526d1be9
						
					
				
				
					commit
					7ed268fef4
				
			@ -17,6 +17,7 @@ class CaptchaStageSerializer(StageSerializer):
 | 
				
			|||||||
            "private_key",
 | 
					            "private_key",
 | 
				
			||||||
            "js_url",
 | 
					            "js_url",
 | 
				
			||||||
            "api_url",
 | 
					            "api_url",
 | 
				
			||||||
 | 
					            "interactive",
 | 
				
			||||||
            "score_min_threshold",
 | 
					            "score_min_threshold",
 | 
				
			||||||
            "score_max_threshold",
 | 
					            "score_max_threshold",
 | 
				
			||||||
            "error_on_invalid_score",
 | 
					            "error_on_invalid_score",
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.9 on 2024-10-30 14:28
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="captchastage",
 | 
				
			||||||
 | 
					            name="interactive",
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -9,11 +9,13 @@ from authentik.flows.models import Stage
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CaptchaStage(Stage):
 | 
					class CaptchaStage(Stage):
 | 
				
			||||||
    """Verify the user is human using Google's reCaptcha."""
 | 
					    """Verify the user is human using Google's reCaptcha/other compatible CAPTCHA solutions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider."))
 | 
					    public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider."))
 | 
				
			||||||
    private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider."))
 | 
					    private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    interactive = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    score_min_threshold = models.FloatField(default=0.5)  # Default values for reCaptcha
 | 
					    score_min_threshold = models.FloatField(default=0.5)  # Default values for reCaptcha
 | 
				
			||||||
    score_max_threshold = models.FloatField(default=1.0)  # Default values for reCaptcha
 | 
					    score_max_threshold = models.FloatField(default=1.0)  # Default values for reCaptcha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
from django.http.response import HttpResponse
 | 
					from django.http.response import HttpResponse
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from requests import RequestException
 | 
					from requests import RequestException
 | 
				
			||||||
from rest_framework.fields import CharField
 | 
					from rest_framework.fields import BooleanField, CharField
 | 
				
			||||||
from rest_framework.serializers import ValidationError
 | 
					from rest_framework.serializers import ValidationError
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,10 +24,12 @@ PLAN_CONTEXT_CAPTCHA = "captcha"
 | 
				
			|||||||
class CaptchaChallenge(WithUserInfoChallenge):
 | 
					class CaptchaChallenge(WithUserInfoChallenge):
 | 
				
			||||||
    """Site public key"""
 | 
					    """Site public key"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    site_key = CharField()
 | 
					 | 
				
			||||||
    js_url = CharField()
 | 
					 | 
				
			||||||
    component = CharField(default="ak-stage-captcha")
 | 
					    component = CharField(default="ak-stage-captcha")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    site_key = CharField(required=True)
 | 
				
			||||||
 | 
					    js_url = CharField(required=True)
 | 
				
			||||||
 | 
					    interactive = BooleanField(required=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str):
 | 
					def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str):
 | 
				
			||||||
    """Validate captcha token"""
 | 
					    """Validate captcha token"""
 | 
				
			||||||
@ -103,6 +105,7 @@ class CaptchaStageView(ChallengeStageView):
 | 
				
			|||||||
            data={
 | 
					            data={
 | 
				
			||||||
                "js_url": self.executor.current_stage.js_url,
 | 
					                "js_url": self.executor.current_stage.js_url,
 | 
				
			||||||
                "site_key": self.executor.current_stage.public_key,
 | 
					                "site_key": self.executor.current_stage.public_key,
 | 
				
			||||||
 | 
					                "interactive": self.executor.current_stage.interactive,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -223,6 +223,7 @@ class IdentificationStageView(ChallengeStageView):
 | 
				
			|||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        "js_url": current_stage.captcha_stage.js_url,
 | 
					                        "js_url": current_stage.captcha_stage.js_url,
 | 
				
			||||||
                        "site_key": current_stage.captcha_stage.public_key,
 | 
					                        "site_key": current_stage.captcha_stage.public_key,
 | 
				
			||||||
 | 
					                        "interactive": current_stage.captcha_stage.interactive,
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if current_stage.captcha_stage
 | 
					                    if current_stage.captcha_stage
 | 
				
			||||||
                    else None
 | 
					                    else None
 | 
				
			||||||
 | 
				
			|||||||
@ -9781,6 +9781,10 @@
 | 
				
			|||||||
                    "minLength": 1,
 | 
					                    "minLength": 1,
 | 
				
			||||||
                    "title": "Api url"
 | 
					                    "title": "Api url"
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                "interactive": {
 | 
				
			||||||
 | 
					                    "type": "boolean",
 | 
				
			||||||
 | 
					                    "title": "Interactive"
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
                "score_min_threshold": {
 | 
					                "score_min_threshold": {
 | 
				
			||||||
                    "type": "number",
 | 
					                    "type": "number",
 | 
				
			||||||
                    "title": "Score min threshold"
 | 
					                    "title": "Score min threshold"
 | 
				
			||||||
 | 
				
			|||||||
@ -39220,7 +39220,10 @@ components:
 | 
				
			|||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
        js_url:
 | 
					        js_url:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
 | 
					        interactive:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
      required:
 | 
					      required:
 | 
				
			||||||
 | 
					      - interactive
 | 
				
			||||||
      - js_url
 | 
					      - js_url
 | 
				
			||||||
      - pending_user
 | 
					      - pending_user
 | 
				
			||||||
      - pending_user_avatar
 | 
					      - pending_user_avatar
 | 
				
			||||||
@ -39276,6 +39279,8 @@ components:
 | 
				
			|||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
        api_url:
 | 
					        api_url:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
 | 
					        interactive:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
        score_min_threshold:
 | 
					        score_min_threshold:
 | 
				
			||||||
          type: number
 | 
					          type: number
 | 
				
			||||||
          format: double
 | 
					          format: double
 | 
				
			||||||
@ -39322,6 +39327,8 @@ components:
 | 
				
			|||||||
        api_url:
 | 
					        api_url:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          minLength: 1
 | 
					          minLength: 1
 | 
				
			||||||
 | 
					        interactive:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
        score_min_threshold:
 | 
					        score_min_threshold:
 | 
				
			||||||
          type: number
 | 
					          type: number
 | 
				
			||||||
          format: double
 | 
					          format: double
 | 
				
			||||||
@ -47732,6 +47739,8 @@ components:
 | 
				
			|||||||
        api_url:
 | 
					        api_url:
 | 
				
			||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          minLength: 1
 | 
					          minLength: 1
 | 
				
			||||||
 | 
					        interactive:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
        score_min_threshold:
 | 
					        score_min_threshold:
 | 
				
			||||||
          type: number
 | 
					          type: number
 | 
				
			||||||
          format: double
 | 
					          format: double
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
 | 
				
			|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
import { first } from "@goauthentik/common/utils";
 | 
					import { first } from "@goauthentik/common/utils";
 | 
				
			||||||
import "@goauthentik/components/ak-number-input";
 | 
					import "@goauthentik/components/ak-number-input";
 | 
				
			||||||
 | 
					import "@goauthentik/components/ak-switch-input";
 | 
				
			||||||
import "@goauthentik/elements/forms/FormGroup";
 | 
					import "@goauthentik/elements/forms/FormGroup";
 | 
				
			||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
					import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -80,6 +81,15 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
 | 
				
			|||||||
                            )}
 | 
					                            )}
 | 
				
			||||||
                        </p>
 | 
					                        </p>
 | 
				
			||||||
                    </ak-form-element-horizontal>
 | 
					                    </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
 | 
					                    <ak-number-input
 | 
				
			||||||
                        label=${msg("Score minimum threshold")}
 | 
					                        label=${msg("Score minimum threshold")}
 | 
				
			||||||
                        required
 | 
					                        required
 | 
				
			||||||
 | 
				
			|||||||
@ -10,10 +10,14 @@ export const DOM_PURIFY_STRICT: DOMPurify.Config = {
 | 
				
			|||||||
    ALLOWED_TAGS: ["#text"],
 | 
					    ALLOWED_TAGS: ["#text"],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function renderStatic(input: TemplateResult): Promise<string> {
 | 
				
			||||||
 | 
					    return await collectResult(render(input));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function purify(input: TemplateResult): TemplateResult {
 | 
					export function purify(input: TemplateResult): TemplateResult {
 | 
				
			||||||
    return html`${until(
 | 
					    return html`${until(
 | 
				
			||||||
        (async () => {
 | 
					        (async () => {
 | 
				
			||||||
            const rendered = await collectResult(render(input));
 | 
					            const rendered = await renderStatic(input);
 | 
				
			||||||
            const purified = DOMPurify.sanitize(rendered);
 | 
					            const purified = DOMPurify.sanitize(rendered);
 | 
				
			||||||
            return html`${unsafeHTML(purified)}`;
 | 
					            return html`${unsafeHTML(purified)}`;
 | 
				
			||||||
        })(),
 | 
					        })(),
 | 
				
			||||||
 | 
				
			|||||||
@ -107,7 +107,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
 | 
				
			|||||||
                    ?loading="${this.authenticating}"
 | 
					                    ?loading="${this.authenticating}"
 | 
				
			||||||
                    header=${this.authenticating
 | 
					                    header=${this.authenticating
 | 
				
			||||||
                        ? msg("Authenticating...")
 | 
					                        ? msg("Authenticating...")
 | 
				
			||||||
                        : this.errorMessage || msg("Failed to authenticate")}
 | 
					                        : this.errorMessage || msg("Loading")}
 | 
				
			||||||
                    icon="fa-times"
 | 
					                    icon="fa-times"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                </ak-empty-state>
 | 
					                </ak-empty-state>
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ import "../../../stories/flow-interface";
 | 
				
			|||||||
import "./CaptchaStage";
 | 
					import "./CaptchaStage";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    title: "Flow / Stages / CaptchaStage",
 | 
					    title: "Flow / Stages / Captcha",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LoadingNoChallenge = () => {
 | 
					export const LoadingNoChallenge = () => {
 | 
				
			||||||
@ -25,7 +25,8 @@ export const LoadingNoChallenge = () => {
 | 
				
			|||||||
    </ak-storybook-interface>`;
 | 
					    </ak-storybook-interface>`;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ChallengeGoogleReCaptcha: StoryObj = {
 | 
					function captchaFactory(challenge: CaptchaChallenge): StoryObj {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
        render: ({ theme, challenge }) => {
 | 
					        render: ({ theme, challenge }) => {
 | 
				
			||||||
            return html`<ak-storybook-interface theme=${theme}>
 | 
					            return html`<ak-storybook-interface theme=${theme}>
 | 
				
			||||||
                <div class="pf-c-login">
 | 
					                <div class="pf-c-login">
 | 
				
			||||||
@ -38,12 +39,7 @@ export const ChallengeGoogleReCaptcha: StoryObj = {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        args: {
 | 
					        args: {
 | 
				
			||||||
            theme: "automatic",
 | 
					            theme: "automatic",
 | 
				
			||||||
        challenge: {
 | 
					            challenge: challenge,
 | 
				
			||||||
            pendingUser: "foo",
 | 
					 | 
				
			||||||
            pendingUserAvatar: "https://picsum.photos/64",
 | 
					 | 
				
			||||||
            jsUrl: "https://www.google.com/recaptcha/api.js",
 | 
					 | 
				
			||||||
            siteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
 | 
					 | 
				
			||||||
        } as CaptchaChallenge,
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        argTypes: {
 | 
					        argTypes: {
 | 
				
			||||||
            theme: {
 | 
					            theme: {
 | 
				
			||||||
@ -53,64 +49,36 @@ export const ChallengeGoogleReCaptcha: StoryObj = {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
};
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ChallengeHCaptcha: StoryObj = {
 | 
					export const ChallengeHCaptcha = captchaFactory({
 | 
				
			||||||
    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",
 | 
					    pendingUser: "foo",
 | 
				
			||||||
    pendingUserAvatar: "https://picsum.photos/64",
 | 
					    pendingUserAvatar: "https://picsum.photos/64",
 | 
				
			||||||
    jsUrl: "https://js.hcaptcha.com/1/api.js",
 | 
					    jsUrl: "https://js.hcaptcha.com/1/api.js",
 | 
				
			||||||
    siteKey: "10000000-ffff-ffff-ffff-000000000001",
 | 
					    siteKey: "10000000-ffff-ffff-ffff-000000000001",
 | 
				
			||||||
        } as CaptchaChallenge,
 | 
					    interactive: true,
 | 
				
			||||||
    },
 | 
					} as CaptchaChallenge);
 | 
				
			||||||
    argTypes: {
 | 
					 | 
				
			||||||
        theme: {
 | 
					 | 
				
			||||||
            options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
 | 
					 | 
				
			||||||
            control: {
 | 
					 | 
				
			||||||
                type: "select",
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ChallengeTurnstile: StoryObj = {
 | 
					// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
 | 
				
			||||||
    render: ({ theme, challenge }) => {
 | 
					export const ChallengeTurnstileVisible = captchaFactory({
 | 
				
			||||||
        return html`<ak-storybook-interface theme=${theme}>
 | 
					    pendingUser: "foo",
 | 
				
			||||||
            <div class="pf-c-login">
 | 
					    pendingUserAvatar: "https://picsum.photos/64",
 | 
				
			||||||
                <div class="pf-c-login__container">
 | 
					    jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
 | 
				
			||||||
                    <div class="pf-c-login__main">
 | 
					    siteKey: "1x00000000000000000000AA",
 | 
				
			||||||
                        <ak-stage-captcha .challenge=${challenge}></ak-stage-captcha>
 | 
					    interactive: true,
 | 
				
			||||||
                    </div>
 | 
					} as CaptchaChallenge);
 | 
				
			||||||
                </div></div
 | 
					export const ChallengeTurnstileInvisible = captchaFactory({
 | 
				
			||||||
        ></ak-storybook-interface>`;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    args: {
 | 
					 | 
				
			||||||
        theme: "automatic",
 | 
					 | 
				
			||||||
        challenge: {
 | 
					 | 
				
			||||||
    pendingUser: "foo",
 | 
					    pendingUser: "foo",
 | 
				
			||||||
    pendingUserAvatar: "https://picsum.photos/64",
 | 
					    pendingUserAvatar: "https://picsum.photos/64",
 | 
				
			||||||
    jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
 | 
					    jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
 | 
				
			||||||
    siteKey: "1x00000000000000000000BB",
 | 
					    siteKey: "1x00000000000000000000BB",
 | 
				
			||||||
        } as CaptchaChallenge,
 | 
					    interactive: true,
 | 
				
			||||||
    },
 | 
					} as CaptchaChallenge);
 | 
				
			||||||
    argTypes: {
 | 
					export const ChallengeTurnstileForce = captchaFactory({
 | 
				
			||||||
        theme: {
 | 
					    pendingUser: "foo",
 | 
				
			||||||
            options: [UiThemeEnum.Automatic, UiThemeEnum.Light, UiThemeEnum.Dark],
 | 
					    pendingUserAvatar: "https://picsum.photos/64",
 | 
				
			||||||
            control: {
 | 
					    jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
 | 
				
			||||||
                type: "select",
 | 
					    siteKey: "3x00000000000000000000FF",
 | 
				
			||||||
            },
 | 
					    interactive: true,
 | 
				
			||||||
        },
 | 
					} as CaptchaChallenge);
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,17 @@
 | 
				
			|||||||
///<reference types="@hcaptcha/types"/>
 | 
					///<reference types="@hcaptcha/types"/>
 | 
				
			||||||
 | 
					import { renderStatic } from "@goauthentik/common/purify";
 | 
				
			||||||
import "@goauthentik/elements/EmptyState";
 | 
					import "@goauthentik/elements/EmptyState";
 | 
				
			||||||
import "@goauthentik/elements/forms/FormElement";
 | 
					import "@goauthentik/elements/forms/FormElement";
 | 
				
			||||||
 | 
					import { randomId } from "@goauthentik/elements/utils/randomId";
 | 
				
			||||||
import "@goauthentik/flow/FormStatic";
 | 
					import "@goauthentik/flow/FormStatic";
 | 
				
			||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
					import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
				
			||||||
import type { TurnstileObject } from "turnstile-types";
 | 
					import type { TurnstileObject } from "turnstile-types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					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 { customElement, property, state } from "lit/decorators.js";
 | 
				
			||||||
import { ifDefined } from "lit/directives/if-defined.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 PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
				
			||||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
 | 
					import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
 | 
				
			||||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
 | 
					import PFLogin from "@patternfly/patternfly/components/Login/login.css";
 | 
				
			||||||
@ -24,12 +25,22 @@ interface TurnstileWindow extends Window {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
type TokenHandler = (token: string) => void;
 | 
					type TokenHandler = (token: string) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const captchaContainerID = "captcha-container";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ak-stage-captcha")
 | 
					@customElement("ak-stage-captcha")
 | 
				
			||||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
					export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
				
			||||||
    static get styles(): CSSResult[] {
 | 
					    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];
 | 
					    handlers = [this.handleGReCaptcha, this.handleHCaptcha, this.handleTurnstile];
 | 
				
			||||||
@ -38,14 +49,17 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
				
			|||||||
    error?: string;
 | 
					    error?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @state()
 | 
					    @state()
 | 
				
			||||||
    captchaInteractive: boolean = true;
 | 
					    captchaFrame: HTMLIFrameElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @state()
 | 
					    @state()
 | 
				
			||||||
    captchaContainer: HTMLDivElement;
 | 
					    captchaDocumentContainer: HTMLDivElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @state()
 | 
					    @state()
 | 
				
			||||||
    scriptElement?: HTMLScriptElement;
 | 
					    scriptElement?: HTMLScriptElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property({ type: Boolean })
 | 
				
			||||||
 | 
					    embedded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property()
 | 
					    @property()
 | 
				
			||||||
    onTokenChange: TokenHandler = (token: string) => {
 | 
					    onTokenChange: TokenHandler = (token: string) => {
 | 
				
			||||||
        this.host.submit({ component: "ak-stage-captcha", token });
 | 
					        this.host.submit({ component: "ak-stage-captcha", token });
 | 
				
			||||||
@ -53,8 +67,70 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
        this.captchaContainer = document.createElement("div");
 | 
					        this.captchaFrame = document.createElement("iframe");
 | 
				
			||||||
        this.captchaContainer.id = captchaContainerID;
 | 
					        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>) {
 | 
					    updated(changedProperties: PropertyValues<this>) {
 | 
				
			||||||
@ -64,15 +140,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
				
			|||||||
            this.scriptElement.async = true;
 | 
					            this.scriptElement.async = true;
 | 
				
			||||||
            this.scriptElement.defer = true;
 | 
					            this.scriptElement.defer = true;
 | 
				
			||||||
            this.scriptElement.dataset.akCaptchaScript = "true";
 | 
					            this.scriptElement.dataset.akCaptchaScript = "true";
 | 
				
			||||||
            this.scriptElement.onload = () => {
 | 
					            this.scriptElement.onload = async () => {
 | 
				
			||||||
                console.debug("authentik/stages/captcha: script loaded");
 | 
					                console.debug("authentik/stages/captcha: script loaded");
 | 
				
			||||||
                let found = false;
 | 
					                let found = false;
 | 
				
			||||||
                let lastError = undefined;
 | 
					                let lastError = undefined;
 | 
				
			||||||
                this.handlers.forEach((handler) => {
 | 
					                this.handlers.forEach(async (handler) => {
 | 
				
			||||||
                    let handlerFound = false;
 | 
					                    let handlerFound = false;
 | 
				
			||||||
                    try {
 | 
					                    try {
 | 
				
			||||||
                        console.debug(`authentik/stages/captcha[${handler.name}]: trying handler`);
 | 
					                        console.debug(`authentik/stages/captcha[${handler.name}]: trying handler`);
 | 
				
			||||||
                        handlerFound = handler.apply(this);
 | 
					                        handlerFound = await handler.apply(this);
 | 
				
			||||||
                        if (handlerFound) {
 | 
					                        if (handlerFound) {
 | 
				
			||||||
                            console.debug(
 | 
					                            console.debug(
 | 
				
			||||||
                                `authentik/stages/captcha[${handler.name}]: handler succeeded`,
 | 
					                                `authentik/stages/captcha[${handler.name}]: handler succeeded`,
 | 
				
			||||||
@ -96,51 +172,79 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
				
			|||||||
                .querySelectorAll("[data-ak-captcha-script=true]")
 | 
					                .querySelectorAll("[data-ak-captcha-script=true]")
 | 
				
			||||||
                .forEach((el) => el.remove());
 | 
					                .forEach((el) => el.remove());
 | 
				
			||||||
            document.head.appendChild(this.scriptElement);
 | 
					            document.head.appendChild(this.scriptElement);
 | 
				
			||||||
 | 
					            if (!this.challenge.interactive) {
 | 
				
			||||||
 | 
					                document.appendChild(this.captchaDocumentContainer);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleGReCaptcha(): boolean {
 | 
					    async handleGReCaptcha(): Promise<boolean> {
 | 
				
			||||||
        if (!Object.hasOwn(window, "grecaptcha")) {
 | 
					        if (!Object.hasOwn(window, "grecaptcha")) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.captchaInteractive = false;
 | 
					        if (this.challenge.interactive) {
 | 
				
			||||||
        document.body.appendChild(this.captchaContainer);
 | 
					            this.renderFrame(
 | 
				
			||||||
 | 
					                html`<div
 | 
				
			||||||
 | 
					                    class="g-recaptcha"
 | 
				
			||||||
 | 
					                    data-sitekey="${this.challenge.siteKey}"
 | 
				
			||||||
 | 
					                    data-callback="callback"
 | 
				
			||||||
 | 
					                ></div>`,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
            grecaptcha.ready(() => {
 | 
					            grecaptcha.ready(() => {
 | 
				
			||||||
            const captchaId = grecaptcha.render(this.captchaContainer, {
 | 
					                const captchaId = grecaptcha.render(this.captchaDocumentContainer, {
 | 
				
			||||||
                    sitekey: this.challenge.siteKey,
 | 
					                    sitekey: this.challenge.siteKey,
 | 
				
			||||||
                    callback: this.onTokenChange,
 | 
					                    callback: this.onTokenChange,
 | 
				
			||||||
                    size: "invisible",
 | 
					                    size: "invisible",
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                grecaptcha.execute(captchaId);
 | 
					                grecaptcha.execute(captchaId);
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleHCaptcha(): boolean {
 | 
					    async handleHCaptcha(): Promise<boolean> {
 | 
				
			||||||
        if (!Object.hasOwn(window, "hcaptcha")) {
 | 
					        if (!Object.hasOwn(window, "hcaptcha")) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.captchaInteractive = false;
 | 
					        if (this.challenge.interactive) {
 | 
				
			||||||
        document.body.appendChild(this.captchaContainer);
 | 
					            this.renderFrame(
 | 
				
			||||||
        const captchaId = hcaptcha.render(this.captchaContainer, {
 | 
					                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,
 | 
					                sitekey: this.challenge.siteKey,
 | 
				
			||||||
                callback: this.onTokenChange,
 | 
					                callback: this.onTokenChange,
 | 
				
			||||||
                size: "invisible",
 | 
					                size: "invisible",
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            hcaptcha.execute(captchaId);
 | 
					            hcaptcha.execute(captchaId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleTurnstile(): boolean {
 | 
					    async handleTurnstile(): Promise<boolean> {
 | 
				
			||||||
        if (!Object.hasOwn(window, "turnstile")) {
 | 
					        if (!Object.hasOwn(window, "turnstile")) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.captchaInteractive = false;
 | 
					        if (this.challenge.interactive) {
 | 
				
			||||||
        document.body.appendChild(this.captchaContainer);
 | 
					            this.renderFrame(
 | 
				
			||||||
        (window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, {
 | 
					                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,
 | 
					                sitekey: this.challenge.siteKey,
 | 
				
			||||||
                callback: this.onTokenChange,
 | 
					                callback: this.onTokenChange,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -148,13 +252,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
				
			|||||||
        if (this.error) {
 | 
					        if (this.error) {
 | 
				
			||||||
            return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
 | 
					            return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (this.captchaInteractive) {
 | 
					        if (this.challenge.interactive) {
 | 
				
			||||||
            return html`${this.captchaContainer}`;
 | 
					            return html`${this.captchaFrame}`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
 | 
					        return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
 | 
					        if (this.embedded) {
 | 
				
			||||||
 | 
					            if (!this.challenge.interactive) {
 | 
				
			||||||
 | 
					                return html``;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return this.renderBody();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (!this.challenge) {
 | 
					        if (!this.challenge) {
 | 
				
			||||||
            return html`<ak-empty-state loading> </ak-empty-state>`;
 | 
					            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`
 | 
					                ? html`
 | 
				
			||||||
                      <input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
 | 
					                      <input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
 | 
				
			||||||
                      <ak-stage-captcha
 | 
					                      <ak-stage-captcha
 | 
				
			||||||
                          style="visibility: hidden; position:absolute;"
 | 
					 | 
				
			||||||
                          .challenge=${this.challenge.captchaStage}
 | 
					                          .challenge=${this.challenge.captchaStage}
 | 
				
			||||||
                          .onTokenChange=${(token: string) => {
 | 
					                          .onTokenChange=${(token: string) => {
 | 
				
			||||||
                              this.captchaToken = token;
 | 
					                              this.captchaToken = token;
 | 
				
			||||||
                          }}
 | 
					                          }}
 | 
				
			||||||
 | 
					                          embedded
 | 
				
			||||||
                      ></ak-stage-captcha>
 | 
					                      ></ak-stage-captcha>
 | 
				
			||||||
                  `
 | 
					                  `
 | 
				
			||||||
                : nothing}
 | 
					                : nothing}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,15 +2,17 @@
 | 
				
			|||||||
title: Captcha stage
 | 
					title: Captcha stage
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This stage adds a form of verification using [Google's ReCaptcha](https://www.google.com/recaptcha/intro/v3.html) or compatible services. Currently supported implementations:
 | 
					This stage adds a form of verification using [Google's reCAPTCHA](https://www.google.com/recaptcha/intro/v3.html) or compatible services.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-   ReCaptcha
 | 
					Currently supported implementations:
 | 
				
			||||||
-   hCaptcha
 | 
					
 | 
				
			||||||
-   Turnstile
 | 
					-   [Google reCAPTCHA](#google-recaptcha)
 | 
				
			||||||
 | 
					-   [hCaptcha](#hcaptcha)
 | 
				
			||||||
 | 
					-   [Cloudflare Turnstile](#cloudflare-turnstile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Captcha provider configuration
 | 
					## Captcha provider configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Google ReCaptcha
 | 
					### Google reCAPTCHA
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This stage has two required fields: Public key and private key. These can both be acquired at https://www.google.com/recaptcha/admin.
 | 
					This stage has two required fields: Public key and private key. These can both be acquired at https://www.google.com/recaptcha/admin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,10 +20,11 @@ This stage has two required fields: Public key and private key. These can both b
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#### Configuration options
 | 
					#### Configuration options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-   JS URL: `https://www.recaptcha.net/recaptcha/api.js`
 | 
					-   Interactive: Enabled when using reCAPTCHA v3
 | 
				
			||||||
-   API URL: `https://www.recaptcha.net/recaptcha/api/siteverify`
 | 
					 | 
				
			||||||
-   Score minimum threshold: `0.5`
 | 
					-   Score minimum threshold: `0.5`
 | 
				
			||||||
-   Score maximum threshold: `1`
 | 
					-   Score maximum threshold: `1`
 | 
				
			||||||
 | 
					-   JS URL: `https://www.recaptcha.net/recaptcha/api.js`
 | 
				
			||||||
 | 
					-   API URL: `https://www.recaptcha.net/recaptcha/api/siteverify`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### hCaptcha
 | 
					### hCaptcha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,6 +32,7 @@ See https://docs.hcaptcha.com/switch
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#### Configuration options
 | 
					#### Configuration options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   Interactive: Enabled
 | 
				
			||||||
-   JS URL: `https://js.hcaptcha.com/1/api.js`
 | 
					-   JS URL: `https://js.hcaptcha.com/1/api.js`
 | 
				
			||||||
-   API URL: `https://api.hcaptcha.com/siteverify`
 | 
					-   API URL: `https://api.hcaptcha.com/siteverify`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,16 +41,13 @@ See https://docs.hcaptcha.com/switch
 | 
				
			|||||||
-   Score minimum threshold: `0`
 | 
					-   Score minimum threshold: `0`
 | 
				
			||||||
-   Score maximum threshold: `0.5`
 | 
					-   Score maximum threshold: `0.5`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Turnstile
 | 
					### Cloudflare Turnstile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
See https://developers.cloudflare.com/turnstile/get-started/migrating-from-recaptcha
 | 
					See https://developers.cloudflare.com/turnstile/get-started/migrating-from-recaptcha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:::warning
 | 
					 | 
				
			||||||
To use Cloudflare Turnstile, the site must be configured to use the "Invisible" mode, otherwise the widget will be rendered incorrectly.
 | 
					 | 
				
			||||||
:::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Configuration options
 | 
					#### Configuration options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   Interactive: Enabled if the Turnstile instance is configured as visible or managed
 | 
				
			||||||
-   JS URL: `https://challenges.cloudflare.com/turnstile/v0/api.js`
 | 
					-   JS URL: `https://challenges.cloudflare.com/turnstile/v0/api.js`
 | 
				
			||||||
-   API URL: `https://challenges.cloudflare.com/turnstile/v0/siteverify`
 | 
					-   API URL: `https://challenges.cloudflare.com/turnstile/v0/siteverify`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user