From 2d3ba40d2140b760230b23fd3847bedeb62ae269 Mon Sep 17 00:00:00 2001 From: Aravindhan Alagesan Date: Fri, 22 Dec 2023 13:41:22 +0530 Subject: [PATCH 1/4] Creating new form component to support dynamic json in login page for knowledge based authentication Signed-off-by: Aravindhan Alagesan --- oidc-ui/.env | 2 + oidc-ui/.env.development | 3 +- oidc-ui/public/locales/ar.json | 10 + oidc-ui/public/locales/en.json | 11 + oidc-ui/src/common/ErrorBanner.js | 30 +++ oidc-ui/src/components/Form.js | 262 +++++++++++++++++++++++ oidc-ui/src/constants/clientConstants.js | 7 +- oidc-ui/src/pages/Login.js | 28 ++- oidc-ui/src/services/walletService.js | 1 + 9 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 oidc-ui/src/common/ErrorBanner.js create mode 100644 oidc-ui/src/components/Form.js diff --git a/oidc-ui/.env b/oidc-ui/.env index e9bbbf1c5..9eb0daae8 100644 --- a/oidc-ui/.env +++ b/oidc-ui/.env @@ -46,3 +46,5 @@ REACT_APP_WALLET_LOGO_URL="" REACT_APP_CONSENT_SCREEN_TIME_OUT_BUFFER_IN_SEC=5 REACT_APP_WALLET_QR_CODE_AUTO_REFRESH_LIMIT=3 + +REACT_APP_ESIGNET_API_URL=/v1/esignet \ No newline at end of file diff --git a/oidc-ui/.env.development b/oidc-ui/.env.development index 0f8fb6fac..4e643e8e9 100644 --- a/oidc-ui/.env.development +++ b/oidc-ui/.env.development @@ -1 +1,2 @@ -REACT_APP_SBI_ENV=Staging \ No newline at end of file +REACT_APP_SBI_ENV=Staging +REACT_APP_ESIGNET_API_URL=/v1/esignet \ No newline at end of file diff --git a/oidc-ui/public/locales/ar.json b/oidc-ui/public/locales/ar.json index 1645fd277..fd9af1e68 100644 --- a/oidc-ui/public/locales/ar.json +++ b/oidc-ui/public/locales/ar.json @@ -71,6 +71,16 @@ "uin_placeholder": "VID", "password_placeholder": "كلمة المرور" }, + "Form": { + "sign_in_with_details": "Login with Details", + "login": "Login", + "policyNumber": "Enter Policy Number", + "name": "Enter Name", + "dob": "Enter DOB", + "policyNumber_placeholder": "Policy Number", + "name_placeholder": "Name", + "dob_placeholder": "DOB" + }, "LoginQRCode": { "scan_with_wallet": "قم بالمسح باستخدام {{walletName}} لتسجيل الدخول", "dont_have_wallet": "؟{{walletName}} ليس لديك", diff --git a/oidc-ui/public/locales/en.json b/oidc-ui/public/locales/en.json index 623afcab9..b51cc5f4c 100644 --- a/oidc-ui/public/locales/en.json +++ b/oidc-ui/public/locales/en.json @@ -71,6 +71,16 @@ "uin_placeholder": "VID", "password_placeholder": "Password" }, + "Form": { + "sign_in_with_details": "Login with Details", + "login": "Login", + "policyNumber": "Enter Policy Number", + "name": "Enter Name", + "dob": "Enter DOB", + "policyNumber_placeholder": "Policy Number", + "name_placeholder": "Name", + "dob_placeholder": "DOB" + }, "LoginQRCode": { "scan_with_wallet": "Scan with {{walletName}} to login", "dont_have_wallet": "Don't Have {{walletName}}?", @@ -107,6 +117,7 @@ "QRCode": "QR Code", "OTP": "OTP", "PWD": "Password", + "KBA": "Details", "preferred_mode_of_login": "Select a preferred mode of login", "more_ways_to_sign_in": "More ways to sign in", "or": "OR", diff --git a/oidc-ui/src/common/ErrorBanner.js b/oidc-ui/src/common/ErrorBanner.js new file mode 100644 index 000000000..bf944655a --- /dev/null +++ b/oidc-ui/src/common/ErrorBanner.js @@ -0,0 +1,30 @@ +import { useTranslation } from "react-i18next"; + +const ErrorBanner = ({ + showBanner, + errorCode, + onCloseHandle, + customClass = "", + i18nKeyPrefix = "errors", +}) => { + const { t } = useTranslation("translation", { keyPrefix: i18nKeyPrefix }); + + return showBanner && ( +
+
{t(errorCode)}
+ +
+ ); +}; + +export default ErrorBanner; diff --git a/oidc-ui/src/components/Form.js b/oidc-ui/src/components/Form.js new file mode 100644 index 000000000..1304cbf8a --- /dev/null +++ b/oidc-ui/src/components/Form.js @@ -0,0 +1,262 @@ +import { useEffect, useState, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import ErrorIndicator from "../common/ErrorIndicator"; +import LoadingIndicator from "../common/LoadingIndicator"; +import { + buttonTypes, + challengeFormats, + challengeTypes, + configurationKeys, +} from "../constants/clientConstants"; +import { LoadingStates as states } from "../constants/states"; +import FormAction from "./FormAction"; +import InputWithImage from "./InputWithImage"; +import ReCAPTCHA from "react-google-recaptcha"; +import ErrorBanner from "../common/ErrorBanner"; + +let fieldsState = {}; + +export default function Form({ + authService, + openIDConnectService, + handleBackButtonClick, + i18nKeyPrefix = "Form", +}) { + const { t, i18n } = useTranslation("translation", { + keyPrefix: i18nKeyPrefix, + }); + + const inputCustomClass = + "h-10 border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[hsla(0, 0%, 51%)] focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-muted-light-gray shadow-none"; + + const fields = [{"id":"policyNumber","type":"text","format":""},{"id":"name","type":"text","format":""},{"id":"dob","type":"date","format":"dd\/mm\/yyyy"}]; + fields.forEach((field) => (fieldsState["Form_" + field.id] = "")); + const post_AuthenticateUser = authService.post_AuthenticateUser; + const buildRedirectParams = authService.buildRedirectParams; + + const [loginState, setLoginState] = useState(fieldsState); + const [error, setError] = useState(null); + const [errorBanner, setErrorBanner] = useState([]); + const [status, setStatus] = useState(states.LOADED); + const [invalidState, setInvalidState] = useState(true); + + useEffect(() => { + }, []); + + + const navigate = useNavigate(); + + const handleChange = (e) => { + setLoginState({ ...loginState, [e.target.id]: e.target.value }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + authenticateUser(); + }; + const captchaEnableComponents = + openIDConnectService.getEsignetConfiguration( + configurationKeys.captchaEnableComponents + ) ?? process.env.REACT_APP_CAPTCHA_ENABLE; + + const captchaEnableComponentsList = captchaEnableComponents + .split(",") + .map((x) => x.trim().toLowerCase()); + + const [showCaptcha, setShowCaptcha] = useState( + captchaEnableComponentsList.indexOf("pwd") !== -1 + ); + + const captchaSiteKey = + openIDConnectService.getEsignetConfiguration( + configurationKeys.captchaSiteKey + ) ?? process.env.REACT_APP_CAPTCHA_SITE_KEY; + + const [captchaToken, setCaptchaToken] = useState(null); + const _reCaptchaRef = useRef(null); + const handleCaptchaChange = (value) => { + setCaptchaToken(value); + }; + + //Handle Login API Integration here + const authenticateUser = async () => { + try { + let transactionId = openIDConnectService.getTransactionId(); + let uin = loginState["Form_policyNumber"]; + let challengeManipulate = {}; + fields.forEach(function(field) { + if(field.id !== "policyNumber"){ + challengeManipulate[field.id] = loginState["Form_"+field.id] + } + }); + console.log("challengeManipulate>>>"+challengeManipulate); + let challenge = btoa(JSON.stringify(challengeManipulate)); + + let challengeList = [ + { + authFactorType: "KBA", + challenge: challenge, + format: "base64url-encoded-json", + }, + ]; + + setStatus(states.LOADING); + const authenticateResponse = await post_AuthenticateUser( + transactionId, + uin, + challengeList, + captchaToken + ); + + setStatus(states.LOADED); + + const { response, errors } = authenticateResponse; + + if (errors != null && errors.length > 0) { + setError({ + prefix: "authentication_failed_msg", + errorCode: errors[0].errorCode, + defaultMsg: errors[0].errorMessage, + }); + return; + } else { + setError(null); + + let nonce = openIDConnectService.getNonce(); + let state = openIDConnectService.getState(); + + let params = buildRedirectParams( + nonce, + state, + openIDConnectService.getOAuthDetails(), + response.consentAction + ); + + navigate(process.env.PUBLIC_URL + "/consent" + params, { + replace: true, + }); + } + } catch (error) { + setError({ + prefix: "authentication_failed_msg", + errorCode: error.message, + defaultMsg: error.message, + }); + setStatus(states.ERROR); + } + }; + + useEffect(() => { + let loadComponent = async () => { + i18n.on("languageChanged", () => { + if (showCaptcha) { + //to rerender recaptcha widget on language change + setShowCaptcha(false); + setTimeout(() => { + setShowCaptcha(true); + }, 1); + } + }); + }; + + loadComponent(); + }, []); + + useEffect(() => { + setInvalidState(!Object.values(loginState).every((value) => value?.trim())); + }, [loginState]); + + const onCloseHandle = () => { + let tempBanner = errorBanner.map((_) => _); + tempBanner[0].show = false; + setErrorBanner(tempBanner); + }; + + return ( + <> +
+
+ +
+
+

+ {t("sign_in_with_details")} +

+
+
+ + {errorBanner.length > 0 && ( + + )} + +
+ {fields.map((field) => ( +
+ +
+ ))} + + {showCaptcha && ( +
+ +
+ )} + + 0) || + (showCaptcha && captchaToken === null) + } + /> + + {status === states.LOADING && ( +
+ +
+ )} + {status !== states.LOADING && error && ( + + )} + + ); +} diff --git a/oidc-ui/src/constants/clientConstants.js b/oidc-ui/src/constants/clientConstants.js index 2d46b274a..2d00c1fbd 100644 --- a/oidc-ui/src/constants/clientConstants.js +++ b/oidc-ui/src/constants/clientConstants.js @@ -26,7 +26,8 @@ const validAuthFactors = { OTP: "OTP", BIO: "BIO", PWD: "PWD", - WLA: "WLA" + WLA: "WLA", + KBA: "KBA" }; const buttonTypes = { @@ -76,7 +77,9 @@ const configurationKeys = { consentScreenExpireInSec: "consent.screen.timeout-in-secs", consentScreenTimeOutBufferInSec: "consent.screen.timeout-buffer-in-secs", walletQrCodeAutoRefreshLimit: "wallet.qr-code.auto-refresh-limit", - walletConfig: "wallet.config" + walletConfig: "wallet.config", + authFactorKnowledgeFieldDetails: "auth.factor.knowledge.field-details", + authFactorKnowledgeIndividualIdField: "auth.factor.knowledge.individual-id-field" }; export { diff --git a/oidc-ui/src/pages/Login.js b/oidc-ui/src/pages/Login.js index 9d6f33cf7..02cb6905e 100644 --- a/oidc-ui/src/pages/Login.js +++ b/oidc-ui/src/pages/Login.js @@ -22,6 +22,7 @@ import { Buffer } from "buffer"; import openIDConnectService from "../services/openIDConnectService"; import DefaultError from "../components/DefaultError"; import Password from "../components/Password"; +import Form from "../components/Form"; function InitiateL1Biometrics(openIDConnectService, handleBackButtonClick) { return React.createElement(L1Biometrics, { @@ -52,6 +53,15 @@ function InitiatePassword(openIDConnectService, handleBackButtonClick) { }); } +function InitiateForm(openIDConnectService, handleBackButtonClick) { + return React.createElement(Form, { + authService: new authService(openIDConnectService), + openIDConnectService: openIDConnectService, + handleBackButtonClick: handleBackButtonClick, + }); +} + + function InitiateOtp(openIDConnectService, handleBackButtonClick) { return React.createElement(Otp, { param: otpFields, @@ -113,12 +123,12 @@ function createDynamicLoginElements( return InitiatePassword(oidcService, handleBackButtonClick); } + if (authFactorType === validAuthFactors.KBA) { + return InitiateForm(oidcService, handleBackButtonClick); + } + if (authFactorType === validAuthFactors.WLA) { - return InitiateLinkedWallet( - authFactor, - oidcService, - handleBackButtonClick - ); + return InitiateLinkedWallet(authFactor, oidcService, handleBackButtonClick); } // default element @@ -130,7 +140,7 @@ export default function LoginPage({ i18nKeyPrefix = "header" }) { const [compToShow, setCompToShow] = useState(null); const [clientLogoURL, setClientLogoURL] = useState(null); const [clientName, setClientName] = useState(null); - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const location = useLocation(); var decodeOAuth = Buffer.from(location.hash ?? "", "base64")?.toString(); @@ -164,8 +174,7 @@ export default function LoginPage({ i18nKeyPrefix = "header" }) { setCompToShow( createDynamicLoginElements( authFactor, - oidcService, - handleBackButtonClick + oidcService ) ); }; @@ -188,9 +197,12 @@ export default function LoginPage({ i18nKeyPrefix = "header" }) { heading={t("login_heading", { idProviderName: window._env_.DEFAULT_ID_PROVIDER_NAME, })} + subheading={t("login_subheading")} clientLogoPath={clientLogoURL} clientName={clientName} component={compToShow} + oidcService={oidcService} + authService={new authService(null)} /> ); diff --git a/oidc-ui/src/services/walletService.js b/oidc-ui/src/services/walletService.js index 6df3529f7..b70566183 100644 --- a/oidc-ui/src/services/walletService.js +++ b/oidc-ui/src/services/walletService.js @@ -9,6 +9,7 @@ const modalityIconPath = { WALLET: "images/wallet_icon.svg", BIO: "images/bio_icon.svg", PWD: "images/sign_in_with_otp.png", + KBA: "images/sign_in_with_otp.png", }; const wlaToAuthfactor = (wla) => { From 3d5460c2b89dedac4460b61d3e19ede3b061a182 Mon Sep 17 00:00:00 2001 From: Aravindhan Alagesan Date: Fri, 22 Dec 2023 14:10:02 +0530 Subject: [PATCH 2/4] Creating new form component to support dynamic json in login page for knowledge based authentication Signed-off-by: Aravindhan Alagesan --- oidc-ui/src/components/Form.js | 5 ++--- oidc-ui/src/constants/clientConstants.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/oidc-ui/src/components/Form.js b/oidc-ui/src/components/Form.js index 1304cbf8a..5c5e21562 100644 --- a/oidc-ui/src/components/Form.js +++ b/oidc-ui/src/components/Form.js @@ -30,7 +30,7 @@ export default function Form({ const inputCustomClass = "h-10 border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[hsla(0, 0%, 51%)] focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-muted-light-gray shadow-none"; - const fields = [{"id":"policyNumber","type":"text","format":""},{"id":"name","type":"text","format":""},{"id":"dob","type":"date","format":"dd\/mm\/yyyy"}]; + const fields = openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeFieldDetails) ?? []; fields.forEach((field) => (fieldsState["Form_" + field.id] = "")); const post_AuthenticateUser = authService.post_AuthenticateUser; const buildRedirectParams = authService.buildRedirectParams; @@ -83,14 +83,13 @@ export default function Form({ const authenticateUser = async () => { try { let transactionId = openIDConnectService.getTransactionId(); - let uin = loginState["Form_policyNumber"]; + let uin = loginState["Form_"+openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeIndividualIdField) ?? ""]; let challengeManipulate = {}; fields.forEach(function(field) { if(field.id !== "policyNumber"){ challengeManipulate[field.id] = loginState["Form_"+field.id] } }); - console.log("challengeManipulate>>>"+challengeManipulate); let challenge = btoa(JSON.stringify(challengeManipulate)); let challengeList = [ diff --git a/oidc-ui/src/constants/clientConstants.js b/oidc-ui/src/constants/clientConstants.js index 2d00c1fbd..3dbbbc91f 100644 --- a/oidc-ui/src/constants/clientConstants.js +++ b/oidc-ui/src/constants/clientConstants.js @@ -78,8 +78,8 @@ const configurationKeys = { consentScreenTimeOutBufferInSec: "consent.screen.timeout-buffer-in-secs", walletQrCodeAutoRefreshLimit: "wallet.qr-code.auto-refresh-limit", walletConfig: "wallet.config", - authFactorKnowledgeFieldDetails: "auth.factor.knowledge.field-details", - authFactorKnowledgeIndividualIdField: "auth.factor.knowledge.individual-id-field" + authFactorKnowledgeFieldDetails: "auth.factor.kba.field-details", + authFactorKnowledgeIndividualIdField: "auth.factor.kba.individual-id-field" }; export { From 95e1e0d4dafc4e9d753fd159e9566e1894895017 Mon Sep 17 00:00:00 2001 From: Aravindhan Alagesan Date: Fri, 22 Dec 2023 14:12:04 +0530 Subject: [PATCH 3/4] Creating new form component to support dynamic json in login page for knowledge based authentication Signed-off-by: Aravindhan Alagesan --- oidc-ui/src/components/Form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc-ui/src/components/Form.js b/oidc-ui/src/components/Form.js index 5c5e21562..2fef2a8f2 100644 --- a/oidc-ui/src/components/Form.js +++ b/oidc-ui/src/components/Form.js @@ -86,7 +86,7 @@ export default function Form({ let uin = loginState["Form_"+openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeIndividualIdField) ?? ""]; let challengeManipulate = {}; fields.forEach(function(field) { - if(field.id !== "policyNumber"){ + if(field.id !== openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeIndividualIdField)){ challengeManipulate[field.id] = loginState["Form_"+field.id] } }); From dc94b8ce987477e2c857c55ea012e44b584903f9 Mon Sep 17 00:00:00 2001 From: Aravindhan Alagesan Date: Tue, 2 Jan 2024 13:19:31 +0530 Subject: [PATCH 4/4] Removed settimeout and Dynamic id generating format has been changed Signed-off-by: Aravindhan Alagesan --- oidc-ui/src/components/Form.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/oidc-ui/src/components/Form.js b/oidc-ui/src/components/Form.js index 2fef2a8f2..255686dc4 100644 --- a/oidc-ui/src/components/Form.js +++ b/oidc-ui/src/components/Form.js @@ -31,7 +31,7 @@ export default function Form({ "h-10 border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[hsla(0, 0%, 51%)] focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-muted-light-gray shadow-none"; const fields = openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeFieldDetails) ?? []; - fields.forEach((field) => (fieldsState["Form_" + field.id] = "")); + fields.forEach((field) => (fieldsState["_form_" + field.id] = "")); const post_AuthenticateUser = authService.post_AuthenticateUser; const buildRedirectParams = authService.buildRedirectParams; @@ -83,11 +83,11 @@ export default function Form({ const authenticateUser = async () => { try { let transactionId = openIDConnectService.getTransactionId(); - let uin = loginState["Form_"+openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeIndividualIdField) ?? ""]; + let uin = loginState["_form_"+openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeIndividualIdField) ?? ""]; let challengeManipulate = {}; fields.forEach(function(field) { if(field.id !== openIDConnectService.getEsignetConfiguration(configurationKeys.authFactorKnowledgeIndividualIdField)){ - challengeManipulate[field.id] = loginState["Form_"+field.id] + challengeManipulate[field.id] = loginState["_form_"+field.id] } }); let challenge = btoa(JSON.stringify(challengeManipulate)); @@ -150,11 +150,7 @@ export default function Form({ let loadComponent = async () => { i18n.on("languageChanged", () => { if (showCaptcha) { - //to rerender recaptcha widget on language change - setShowCaptcha(false); - setTimeout(() => { - setShowCaptcha(true); - }, 1); + setShowCaptcha(true); } }); }; @@ -205,12 +201,12 @@ export default function Form({ {fields.map((field) => (