Skip to content

Introduce ReCaptcha support in the new dynamic registration portal #8112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quick-forks-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wso2is/identity-apps-core": minor
---

Introduce ReCaptcha support in the new dynamic registration portal
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
<%@ page import="java.io.File" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.nio.file.Files, java.nio.file.Paths, java.io.IOException" %>
<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.SelfRegistrationMgtClient" %>
<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.IdentityManagementEndpointUtil" %>
<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.IdentityManagementEndpointConstants" %>

Expand Down Expand Up @@ -94,6 +93,12 @@
<link rel="preload" href="${pageContext.request.contextPath}/libs/react/react.production.min.js" as="script" />
<link rel="preload" href="${pageContext.request.contextPath}/libs/react/react-dom.production.min.js" as="script" />
<link rel="preload" href="${pageContext.request.contextPath}/js/react-ui-core.min.js" as="script" />

<script>
window.onSubmit = function(token) {
console.log("Got recaptcha token:", token);
};
</script>
</head>
<body class="login-portal layout authentication-portal-layout">
<layout:main layoutName="<%= layout %>" layoutFileRelativePath="<%= layoutFileRelativePath %>" data="<%= layoutData %>" >
Expand Down Expand Up @@ -314,7 +319,7 @@
{ className: "content-container loaded" },
createElement(
DynamicContent, {
elements: components,
contentData: flowData.data && flowData.data,
handleFlowRequest: (actionId, formValues) => {
setComponents([]);
localStorage.setItem("actionTrigger", actionId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import PropTypes from "prop-types";
import React, { forwardRef } from "react";
import RecaptchaAdapter from "./recaptcha-widget-adapter";

const CaptchaWidgetAdapter = ({ component }, ref) => {

switch (component.variant) {
case "RECAPTCHA_V2":
return <RecaptchaAdapter component={ component } ref={ ref }/>;
default:
return null;
}
};

CaptchaWidgetAdapter.propTypes = {
component: PropTypes.object.isRequired
};

export default forwardRef(CaptchaWidgetAdapter);
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import PropTypes from "prop-types";
import React, { forwardRef, useImperativeHandle } from "react";
import { createPortal } from "react-dom";
import useReCaptcha from "../../hooks/use-recaptcha";

const RecaptchaAdapter = ({ component }, ref) => {
const { containerRef, ready, execute, reset, token } = useReCaptcha(
component.config.captchaKey,
component.config.captchaURL
);

useImperativeHandle(
ref,
() => ({
execute,
reset,
ready,

Check warning on line 35 in identity-apps-core/react-ui-core/src/components/adapters/recaptcha-widget-adapter.js

View workflow job for this annotation

GitHub Actions / ⬣ ESLint (STATIC ANALYSIS) (lts/*, 8.7.4)

Expected object keys to be in ascending order. 'ready' should be before 'reset'
token
}),
[ execute, reset, ready, token ]
);

return (
createPortal(
<div
ref={ containerRef }
className="g-recaptcha"
data-sitekey={ component.config.captchaKey }
data-componentid="registeration-page-g-recaptcha"
data-theme="light"
data-tabindex="-1"
data-size="invisible"
data-callback="onRecaptcha"
/>,
document.body
)
);
};

RecaptchaAdapter.propTypes = {
component: PropTypes.shape({
config: PropTypes.shape({
captchaKey: PropTypes.string.isRequired,
captchaURL: PropTypes.string.isRequired
}).isRequired
}).isRequired
};

export default forwardRef(RecaptchaAdapter);
47 changes: 38 additions & 9 deletions identity-apps-core/react-ui-core/src/components/dynamic-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,38 @@
*/

import PropTypes from "prop-types";
import React from "react";
import React, { useMemo, useRef } from "react";
import { Message } from "semantic-ui-react";
import Field from "./field";
import Form from "./form";

const DynamicContent = ({ elements, handleFlowRequest, error }) => {
const DynamicContent = ({ contentData, handleFlowRequest, error }) => {
const recaptchaRef = useRef(null);

const captchaNode = useMemo(() => contentData.components.find(el => el.type === "CAPTCHA"),
[ contentData.components ]);

const isCaptchaEnabled = !!(
contentData.additionalData &&
contentData.additionalData.captchaEnabled &&
contentData.additionalData.captchaEnabled === "true"
);

const handleFlowExecution = async (actionId, formValues) => {
let finalValues = { ...formValues };

if (isCaptchaEnabled && captchaNode && recaptchaRef.current.ready) {
try {
const token = await recaptchaRef.current.execute();

finalValues = { ...finalValues, captchaResponse: token };
} catch (e) {
console.error("ReCAPTCHA failed", e);

Check warning on line 46 in identity-apps-core/react-ui-core/src/components/dynamic-content.js

View workflow job for this annotation

GitHub Actions / ⬣ ESLint (STATIC ANALYSIS) (lts/*, 8.7.4)

Unexpected console statement
}
}

return handleFlowRequest(actionId, finalValues);
};

const renderForm = (form) => {
if (!form) return null;
Expand All @@ -38,7 +64,8 @@
<Form
key={ form.id }
formSchema={ form.components }
onSubmit={ (action, formValues) => handleFlowRequest(action, formValues) }
onSubmit={ (action, formValues) => handleFlowExecution(action, formValues) }
recaptchaRef={ recaptchaRef }
/>
</>
);
Expand All @@ -52,26 +79,28 @@
<Field
key={ element.id }
component={ element }
flowActionHandler={ (action, formValues) => handleFlowRequest(action, formValues) }
flowActionHandler={ (action, formValues) => handleFlowExecution(action, formValues) }
recaptchaRef={ recaptchaRef }
/>
);
};

const renderElements = () => {
return elements.map((element) => {
if (element.type === "FORM" && Array.isArray(element.components)) {
return renderForm(element);
return contentData.components && contentData.components.map((component) => {
if (component.type === "FORM" && Array.isArray(component.components)) {
return renderForm(component);
}

return renderElement(element);
return renderElement(component);
});
};

return <>{ renderElements() }</>;
};

DynamicContent.propTypes = {
elements: PropTypes.array.isRequired,
contentData: PropTypes.object.isRequired,
error: PropTypes.string,
handleFlowRequest: PropTypes.func.isRequired
};

Expand Down
22 changes: 17 additions & 5 deletions identity-apps-core/react-ui-core/src/components/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,20 @@
import React from "react";

import ButtonFieldAdapter from "./adapters/button-field-adapter";
import CaptchaWidgetAdapter from "./adapters/captcha-widget-adapter";
import InputFieldAdapter from "./adapters/input-field-adapter";
import TypographyAdapter from "./adapters/typography-field-adapter";
import DividerAdapter from "./divider";

const Field = ({ component, formState, formStateHandler, formFieldError, flowActionHandler }) => {
const Field = ({
component,
formState,
formStateHandler,
formFieldError,
flowActionHandler,
recaptchaRef

Check warning on line 34 in identity-apps-core/react-ui-core/src/components/field.js

View workflow job for this annotation

GitHub Actions / ⬣ ESLint (STATIC ANALYSIS) (lts/*, 8.7.4)

'recaptchaRef' is missing in props validation
}) => {

switch (component.type) {
case "TYPOGRAPHY":
return <TypographyAdapter component={ component } />;
Expand All @@ -38,9 +47,11 @@
/>
);
case "BUTTON":
return <ButtonFieldAdapter component={ component } handleButtonAction={ flowActionHandler }/>;
return <ButtonFieldAdapter component={ component } handleButtonAction={ flowActionHandler } />;
case "DIVIDER":
return <DividerAdapter component={ component } />;
case "CAPTCHA":
return <CaptchaWidgetAdapter component={ component } ref={ recaptchaRef } />;
default:
return (
<InputFieldAdapter
Expand All @@ -56,9 +67,10 @@
Field.propTypes = {
component: PropTypes.object.isRequired,
flowActionHandler: PropTypes.func,
formFieldError: PropTypes.func.isRequired,
formState: PropTypes.isRequired,
formStateHandler: PropTypes.func.isRequired
formFieldError: PropTypes.func,
formState: PropTypes.object,
formStateHandler: PropTypes.func,
setRecaptchaRef: PropTypes.func
};

export default Field;
29 changes: 14 additions & 15 deletions identity-apps-core/react-ui-core/src/components/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,28 @@
import Field from "./field";
import useDynamicForm from "../hooks/use-dynamic-form";

const Form = ({ formSchema, onSubmit }) => {
const Form = ({ formSchema, onSubmit, recaptchaRef }) => {

Check warning on line 25 in identity-apps-core/react-ui-core/src/components/form.js

View workflow job for this annotation

GitHub Actions / ⬣ ESLint (STATIC ANALYSIS) (lts/*, 8.7.4)

'recaptchaRef' is missing in props validation
const {
formState,
handleChange,
handleFieldError,
handleSubmit
} = useDynamicForm(formSchema);
} = useDynamicForm(formSchema, onSubmit);

return (
<div className="segment-form">
<SemanticForm noValidate onSubmit={ (event) => handleSubmit(onSubmit)(event) } size="large">
{
formSchema.map((field, index) => (
<FormField key={ index } required={ field.config.required }>
<Field
component={ field }
formState={ formState }
formStateHandler={ handleChange }
formFieldError={ handleFieldError }
/>
</FormField>
))
}
<SemanticForm noValidate onSubmit={ handleSubmit } size="large">
{ formSchema.map((field, index) => (
<FormField key={ index } required={ field.config.required }>
<Field
component={ field }
formState={ formState }
formStateHandler={ handleChange }
formFieldError={ handleFieldError }
recaptchaRef={ field.type === "CAPTCHA" ? recaptchaRef : undefined }
/>
</FormField>
)) }
</SemanticForm>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// Google ReCaptcha constants.
export const DEFAULT_RECAPTCHA_SCRIPT_URL = "https://www.google.com/recaptcha/api.js?render=explicit";
export const DEFAULT_RECAPTCHA_SCRIPT_URL_PARAMS = "?render=explicit";
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

import { useCallback, useState } from "react";

const useDynamicForm = (fields) => {
/**
* Custom hook to manage the state and behavior of a dynamic form.
*/
const useDynamicForm = (fields, onSubmit) => {

const [ formState, setFormState ] = useState({
dirtyFields: {},
Expand Down Expand Up @@ -72,7 +75,7 @@ const useDynamicForm = (fields) => {
});
}, [ fields ]);

const handleSubmit = (onSubmit) => (event) => {
const handleSubmit = (event) => {
event.preventDefault();

let errors = [];
Expand All @@ -87,7 +90,7 @@ const useDynamicForm = (fields) => {
});
}

if (field.config.validation) {
if (field.config.validation && fieldValue) {
field.config.validation.forEach(rule => {
if (rule.type === "MIN_LENGTH" && fieldValue.length < rule.value) {
errors.push({
Expand Down
Loading
Loading