0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(experience,core): add recaptcha enterprise to frontend (#7218)

This commit is contained in:
wangsijie 2025-04-01 15:07:33 +08:00 committed by GitHub
parent 58a5b497bf
commit d418ad0caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 94 additions and 1 deletions

View file

@ -110,11 +110,23 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
'https://static.cloudflareinsights.com/',
// Cloudflare Turnstile
'https://challenges.cloudflare.com/turnstile/v0/api.js',
// Google Recaptcha Enterprise
'https://www.google.com/recaptcha/enterprise.js',
// Google Recaptcha static resources
'https://www.gstatic.com/recaptcha/',
// Allow "unsafe-eval" for debugging purpose in non-production environment
...conditionalArray(!isProduction && "'unsafe-eval'"),
],
scriptSrcAttr: ["'unsafe-inline'"],
connectSrc: ["'self'", gsiOrigin, tenantEndpointOrigin, ...developmentOrigins],
connectSrc: [
"'self'",
gsiOrigin,
tenantEndpointOrigin,
// Allow reCAPTCHA API calls
'https://www.google.com/recaptcha/',
'https://www.gstatic.com/recaptcha/',
...developmentOrigins,
],
// WARNING (high risk): Need to allow self-hosted terms of use page loaded in an iframe
frameSrc: ["'self'", 'https:', gsiOrigin],
// Allow being loaded by console preview iframe

View file

@ -0,0 +1,7 @@
@use '@/scss/underscore' as _;
.wrapper {
display: flex;
justify-content: center;
margin-bottom: _.unit(2);
}

View file

@ -0,0 +1,69 @@
import { useEffect, useRef } from 'react';
import styles from './index.module.scss';
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Window {
grecaptcha?: {
enterprise: {
ready: (callback: () => void) => void;
execute: (sitekey: string, options: { action: string }) => Promise<string>;
};
};
}
}
type Props = {
readonly siteKey: string;
readonly onVerify: (token: string) => void;
};
const scriptId = 'recaptcha-enterprise-script';
const ReCaptchaEnterprise = ({ siteKey, onVerify }: Props) => {
const captchaRef = useRef<HTMLDivElement>(null);
const isRendered = useRef(false);
/* eslint-disable @silverhand/fp/no-mutation */
useEffect(() => {
const render = async () => {
if (!window.grecaptcha?.enterprise || !captchaRef.current || isRendered.current) {
return;
}
window.grecaptcha.enterprise.ready(async () => {
const token = await window.grecaptcha?.enterprise.execute(siteKey, {
action: 'interaction',
});
if (token) {
onVerify(token);
}
});
isRendered.current = true;
};
// Check if script already exists
if (document.querySelector(`#${scriptId}`)) {
void render();
return;
}
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${siteKey}`;
script.id = scriptId;
script.async = true;
script.addEventListener('load', () => {
void render();
});
document.body.append(script);
}, [siteKey, onVerify]);
/* eslint-enable @silverhand/fp/no-mutation */
return <div ref={captchaRef} className={styles.wrapper} />;
};
export default ReCaptchaEnterprise;

View file

@ -1,6 +1,7 @@
import { CaptchaType } from '@logto/schemas';
import { useCallback } from 'react';
import ReCaptchaEnterprise from '@/components/ReCaptchaEnterprise';
import Turnstile from '@/components/Turnstile';
import { type SignInExperienceResponse } from '@/types';
@ -21,6 +22,10 @@ const CaptchaBox = ({ setCaptchaToken, captchaConfig }: Props) => {
return <Turnstile siteKey={captchaConfig.siteKey} onVerify={onVerify} />;
}
if (captchaConfig?.type === CaptchaType.RecaptchaEnterprise) {
return <ReCaptchaEnterprise siteKey={captchaConfig.siteKey} onVerify={onVerify} />;
}
return null;
};