Skip to content

Commit

Permalink
add interactive support for all
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
  • Loading branch information
BeryJu committed Oct 29, 2024
1 parent 1ca12de commit 08fced0
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 76 deletions.
11 changes: 4 additions & 7 deletions web/src/flow/stages/captcha/CaptchaStage.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,12 @@ function captchaFactory(challenge: CaptchaChallenge): StoryObj {
};
}

export const ChallengeGoogleReCaptcha = captchaFactory({
pendingUser: "foo",
pendingUserAvatar: "https://picsum.photos/64",
jsUrl: "https://www.google.com/recaptcha/api.js",
siteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
} as CaptchaChallenge);

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);

// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
Expand All @@ -72,16 +66,19 @@ export const ChallengeTurnstileVisible = captchaFactory({
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);
192 changes: 123 additions & 69 deletions web/src/flow/stages/captcha/CaptchaStage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
///<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 "@hcaptcha/types";
import type { TurnstileObject } from "turnstile-types";

import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, css, 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";

Expand All @@ -19,6 +20,9 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";

import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";

interface TurnstileWindow extends Window {
turnstile: TurnstileObject;
}
type TokenHandler = (token: string) => void;

@customElement("ak-stage-captcha")
Expand All @@ -44,10 +48,10 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
error?: string;

@state()
captchaInteractive: boolean = true;
captchaFrame: HTMLIFrameElement;

@state()
captchaContainer: HTMLIFrameElement;
captchaDocumentContainer: HTMLDivElement;

@state()
scriptElement?: HTMLScriptElement;
Expand All @@ -59,9 +63,68 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe

constructor() {
super();
this.captchaContainer = document.createElement("iframe");
this.captchaContainer.src = "about:blank";
this.captchaContainer.id = `ak-captcha-${randomId()}`;
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 {
window.addEventListener("message", this.messageCallback);
}

disconnectedCallback(): void {
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>) {
Expand Down Expand Up @@ -103,97 +166,88 @@ 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);
}
}
}

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, {
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
size: "invisible",
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);
});
grecaptcha.execute(captchaId);
});
}
return true;
}

async handleHCaptcha(): Promise<boolean> {
if (!Object.hasOwn(window, "hcaptcha")) {
return false;
}
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);
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",
});
hcaptcha.execute(captchaId);
}
return true;
}

async handleTurnstile(): Promise<boolean> {
if (!Object.hasOwn(window, "turnstile")) {
return false;
}
this.captchaInteractive = true;
window.addEventListener("message", (event) => {
const msg: {
source?: string;
context?: string;
message: string;
token: string;
} = event.data;
if (msg.source !== "goauthentik.io" || msg.context !== "flow-executor") {
return;
}
if (msg.message !== "captcha") {
return;
}
this.onTokenChange(msg.token);
});
this.captchaContainer.contentWindow?.document.open();
this.captchaContainer.contentWindow?.document.write(
await renderStatic(
html`<!doctype html>
<html>
<body style="display:flex;flex-direction:row;justify-content:center;">
<div
class="cf-turnstile"
data-sitekey="${this.challenge.siteKey}"
data-callback="callback"
></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>
<script>
function callback(token) {
window.parent.postMessage({
message: "captcha",
source: "goauthentik.io",
context: "flow-executor",
token: token,
});
}
</script>
</body>
</html>`,
),
);
this.captchaContainer.contentWindow?.document.close();
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;
}

renderBody() {
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>`;
}
Expand Down

0 comments on commit 08fced0

Please sign in to comment.