Skip to content
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

Adding recaptcha_v3 #2031

Merged
merged 16 commits into from
Mar 27, 2024
Merged
24 changes: 24 additions & 0 deletions blocks/identity-block/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,35 @@
.b-login-form {
@include scss.block-components("login-form");
@include scss.block-properties("login-form");
&__bot-protection-section {
@include scss.block-components("login-form-bot-protection-section");
@include scss.block-properties("login-form-bot-protection-section");
}
&__privacy-statement {
@include scss.block-components("login-form-privacy-statement");
@include scss.block-properties("login-form-privacy-statement");
}
&__sign-up-div {
@include scss.block-components("login-form-sign-up-div");
@include scss.block-properties("login-form-sign-up-div");
}
&__sign-up-button {
@include scss.block-components("login-form-sign-up-button");
@include scss.block-properties("login-form-sign-up-button");
}
&__login-form-error {
@include scss.block-components("login-form-error");
@include scss.block-properties("login-form-error");
}
}

.b-login-links {
@include scss.block-components("login-links");
@include scss.block-properties("login-links");
&__inner-link {
@include scss.block-components("login-links-inner-link");
@include scss.block-properties("login-links-inner-link");
}
}

.b-reset-password {
Expand Down
48 changes: 48 additions & 0 deletions blocks/identity-block/components/bot-challenge-protection/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useEffect } from "react";
import { useIdentity } from "@wpmedia/arc-themes-components";
import useRecaptcha, { RECAPTCHA_V2, RECAPTCHA_V3 } from "../../utils/useRecaptcha";
import ReCAPTCHA from "react-google-recaptcha";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
import RecaptchaV3 from "./reCaptchaV3";

export const ARCXP_CAPTCHA= "ArcXP_captchaToken"

const BotChallengeProtection = ({ challengeIn, setCaptchaToken, className, captchaError, setCaptchaError, resetRecaptcha }) => {
const { isInitialized } = useIdentity();
const { recaptchaVersion, siteKey, isRecaptchaEnabled } = useRecaptcha(challengeIn);

const onChange = (value) => {
setCaptchaToken(value);
setCaptchaError(null);
localStorage.setItem(ARCXP_CAPTCHA, value);
};

if (!isInitialized) {
return null;
}

if (isRecaptchaEnabled && !!siteKey && !!recaptchaVersion) {
if (recaptchaVersion === RECAPTCHA_V2) {
return (
<section
className={`${className}__bot-protection-section`}
data-testid="bot-challege-protection-container"
>
<ReCAPTCHA sitekey={siteKey} onChange={onChange} onExpired={() => {}}/>
{captchaError && <Paragraph>{phrases.t("identity-block.bot-protection-error")}</Paragraph>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting an error when trying to log-in with recaptcha v2 without clicking reCaptcha box. Error says that Paragraph is not defined. I think we need import statement for Paragraph.

</section>
);
}
if (recaptchaVersion === RECAPTCHA_V3) {
return (
<GoogleReCaptchaProvider reCaptchaKey={siteKey} scriptProps={{ async: true }}>
<RecaptchaV3 setCaptchaToken={setCaptchaToken} resetRecaptcha={resetRecaptcha} />
</GoogleReCaptchaProvider>
);
}
} else {
return null;
}
};

export default BotChallengeProtection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useCallback } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { ARCXP_CAPTCHA } from "./index";

const RecaptchaV3 = ({ setCaptchaToken, resetRecaptcha }) => {
const { executeRecaptcha } = useGoogleReCaptcha();

const handleReCaptcha3Verify = useCallback(async () => {
if (!executeRecaptcha) {
console.log("ArcXP - Execute recaptcha not yet available");
return;
}
const token = await executeRecaptcha();
setCaptchaToken(token);
localStorage.setItem(ARCXP_CAPTCHA, token);
}, [executeRecaptcha]);

useEffect(() => {
handleReCaptcha3Verify();
}, [executeRecaptcha, resetRecaptcha]);

return null;
};

export default RecaptchaV3;
4 changes: 2 additions & 2 deletions blocks/identity-block/features/login-links/default.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
import getTranslatedPhrases from "fusion:intl";
import { Link, Stack } from "@wpmedia/arc-themes-components";
import { Link, Paragraph, Stack } from "@wpmedia/arc-themes-components";

const BLOCK_CLASS_NAME = "b-login-links";
const defaultLoginURL = "/account/login/";
Expand Down Expand Up @@ -35,7 +35,7 @@ const LoginLinks = ({ customFields }) => {
<Link href={forgotURL}>{phrases.t("identity-block.login-links-forgot")}</Link>
) : null}
{showSignUp ? (
<Link href={signUpURL}>{phrases.t("identity-block.login-links-signup")}</Link>
<Paragraph>{phrases.t("identity-block.login-links-signup")}<Link href={signUpURL} className={`${BLOCK_CLASS_NAME}__inner-link`}>{phrases.t("identity-block.sign-up-natural")}</Link></Paragraph>
) : null}
</Stack>
);
Expand Down
83 changes: 67 additions & 16 deletions blocks/identity-block/features/login/default.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,47 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
import getProperties from "fusion:properties";
import getTranslatedPhrases from "fusion:intl";
import { Input, useIdentity } from "@wpmedia/arc-themes-components";
import { Input, useIdentity, Paragraph } from "@wpmedia/arc-themes-components";
import HeadlinedSubmitForm from "../../components/headlined-submit-form";
import useLogin from "../../components/login";
import BotChallengeProtection from "../../components/bot-challenge-protection";
import useOIDCLogin from "../../utils/useOIDCLogin";
import validateURL from "../../utils/validate-redirect-url";
import { RECAPTCHA_LOGIN } from "../../utils/useRecaptcha";

const BLOCK_CLASS_NAME = "b-login-form";

export function definedMessageByCode(code) {
return errorCodes[code] || errorCodes["0"];
}

const errorCodes = {
100015: "identity-block.login-form-error.account-is-disabled",
130001: "identity-block.login-form-error.captcha-token-invalid",
130051: "identity-block.login-form-error.unverified-email-address",
100013: "identity-block.login-form-error.max-devices",
0: "identity-block.login-form-error.invalid-email-password",
};

const Login = ({ customFields }) => {
const { redirectURL, redirectToPreviousPage, loggedInPageLocation, OIDC } = customFields;

const url_string = window.location.href;
const url = new URL(url_string);
const urlString = window.location.href;
const url = new URL(urlString);

const { isAdmin, arcSite } = useFusionContext();
const { locale } = getProperties(arcSite);
const phrases = getTranslatedPhrases(locale);

const isOIDC = OIDC && url.searchParams.get("client_id") && url.searchParams.get("response_type") === "code";
const isOIDC =
OIDC && url.searchParams.get("client_id") && url.searchParams.get("response_type") === "code";
const { Identity, isInitialized } = useIdentity();
const [captchaToken, setCaptchaToken] = useState();
const [resetRecaptcha, setResetRecaptcha] = useState(true);
const [error, setError] = useState();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the purpose of this variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @edwardcho1231 similar to reCaptchav2, reCaptchav3 also expires, but in order to grab a new token we need to call executeRecaptcha(). Thus, resetRecaptcha is changing from true to false and viceversa everytime the reCaptcha3 is expired, thus the useEffect() detects this change and we need to grab a new one.

const [captchaError, setCaptchaError] = useState();
const { loginRedirect } = useLogin({
isAdmin,
redirectURL,
Expand All @@ -41,10 +59,14 @@ const Login = ({ customFields }) => {
<HeadlinedSubmitForm
buttonLabel={phrases.t("identity-block.log-in")}
className={BLOCK_CLASS_NAME}
formErrorText={error}
headline={phrases.t("identity-block.log-in")}
onSubmit={({ email, password }) =>
Identity.login(email, password, {rememberMe: true})
headline={phrases.t("identity-block.log-in-headline")}
onSubmit={({ email, password }) => {
setError(null);
setCaptchaError(null);
Identity.login(email, password, {
rememberMe: true,
recaptchaToken: captchaToken,
})
.then(() => {
if (isOIDC) {
loginByOIDC();
Expand All @@ -53,12 +75,29 @@ const Login = ({ customFields }) => {
window.location = validatedURL;
}
})
.catch(() => setError(phrases.t("identity-block.login-form-error")))
}
.catch((e) => {
setResetRecaptcha(!resetRecaptcha);
if (e?.code === "130001") {
setCaptchaError(true);
}
else {
setError(phrases.t(definedMessageByCode(e.code)));
}
if (grecaptcha) {
grecaptcha.reset();
}

});
}}
>
{error ? (
<div className={`${BLOCK_CLASS_NAME}__login-form-error`}>
<Paragraph>{error}</Paragraph>
</div>
) : null}
<Input
autoComplete="email"
label={phrases.t("identity-block.email")}
label={phrases.t("identity-block.email-label")}
name="email"
required
showDefaultError={false}
Expand All @@ -73,6 +112,17 @@ const Login = ({ customFields }) => {
showDefaultError={false}
type="password"
/>
<BotChallengeProtection
className={BLOCK_CLASS_NAME}
challengeIn={RECAPTCHA_LOGIN}
setCaptchaToken={setCaptchaToken}
captchaError={captchaError}
setCaptchaError={setCaptchaError}
resetRecaptcha={resetRecaptcha}
/>
<Paragraph className={`${BLOCK_CLASS_NAME}__privacy-statement`}>
{phrases.t("identity-block.privacy-statement")}
</Paragraph>
</HeadlinedSubmitForm>
);
};
Expand All @@ -98,10 +148,11 @@ Login.propTypes = {
"The URL to which a user would be redirected to if visiting a login page when already logged in.",
}),
OIDC: PropTypes.bool.tag({
name: 'Login with OIDC',
defaultValue: false,
description: 'Used when authenticating a third party site with OIDC PKCE flow. This will use an ArcXp Org as an auth provider',
}),
name: "Login with OIDC",
defaultValue: false,
description:
"Used when authenticating a third party site with OIDC PKCE flow. This will use an ArcXp Org as an auth provider",
}),
}),
};

Expand Down
32 changes: 31 additions & 1 deletion blocks/identity-block/intl.json
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@
"bn": "কোনো অ্যাকাউন্ট নেই?\nসাইন আপ করুন",
"bo": "ཁྱེད་ལ་དྲ་གྲངས་མེད་དམ། ཐོ་འགོད།",
"de": "Sign up for an account.",
"en": "Don't have an account? Sign up",
"en": "Need to create an account? ",
"es": "Sign up for an account.",
"fr": "Sign up for an account.",
"id": "Belum memiliki akun? Daftar",
Expand Down Expand Up @@ -1273,5 +1273,35 @@
"vi": "Tên người dùng",
"zh-CN": "用户名",
"zh-TW": "使用者名稱"
},
"identity-block.bot-protection-error": {
"en": "Please verify that you are not a robot."
},
"identity-block.email-label": {
"en": "Email"
},
"identity-block.log-in-headline": {
"en": "Log in to your account"
},
"identity-block.login-form-error.account-is-disabled": {
"en": "Account is disabled."
},
"identity-block.login-form-error.captcha-token-invalid": {
"en": "Captcha token invalid."
},
"identity-block.login-form-error.unverified-email-address": {
"en": "Email Address is not verified."
},
"identity-block.login-form-error.max-devices": {
"en": "User account has reached the max number of devices."
},
"identity-block.login-form-error.invalid-email-password": {
"en": "Email or password is invalid. Try again."
},
"identity-block.privacy-statement": {
"en": "By creating an account, you agree to the Terms of Service and acknowledge our Privacy Policy."
},
"identity-block.sign-up-natural": {
"en": "Sign up"
}
}
Loading
Loading