diff --git a/.gitignore b/.gitignore index 9849d1b34..adb6451de 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ helm/*/Chart.lock helm/*/charts/ .DS_Store +coverage/ +build/ \ No newline at end of file diff --git a/esignet-service/data/flows/flow-esignet.yaml b/esignet-service/data/flows/flow-esignet.yaml index 37e8020cc..3616be30b 100644 --- a/esignet-service/data/flows/flow-esignet.yaml +++ b/esignet-service/data/flows/flow-esignet.yaml @@ -139,9 +139,49 @@ prompt_password: &prompt_password required: true resourceType: ELEMENT +# Anchor for Captcha component +captcha_box: &captcha_box + id: CAPTCHA_BOX + ref: captcha + type: CUSTOM + category: MISCELLANEOUS + resourceType: ELEMENT + required: true + captcha: + provider: google-recaptcha + siteKey: ${MOSIP_ESIGNET_CAPTCHA_SITE_KEY} + theme: light + size: normal + +# Anchor for otp's required inputs +otp_inputs: &otp_inputs + - ref: username_input + identifier: username + type: TEXT_INPUT + required: true + - ref: CAPTCHA_BOX + identifier: captcha + type: CUSTOM + required: true + +# Anchor for password's required inputs +password_inputs: &password_inputs + - ref: username_input + identifier: username + type: TEXT_INPUT + required: true + - ref: password_input + identifier: password + type: PASSWORD_INPUT + required: true + - ref: CAPTCHA_BOX + identifier: captcha + type: CUSTOM + required: true + # Anchor for Submit button # default id is submit_uin, it can be changed -# default label is COntinue, that also can be changed +# default label is Continue, that also can be changed submit_button: &submit_button id: submit_uin label: Continue @@ -317,15 +357,12 @@ nodes: - *page_title_with_back - *login_id_uin - *prompt_uin + - *captcha_box - <<: *submit_button id: submit_uin label: Get OTP prompts: - - inputs: - - ref: username_input - identifier: username - type: TEXT_INPUT - required: true + - inputs: *otp_inputs action: ref: submit_uin nextNode: send_mosip_otp @@ -353,15 +390,12 @@ nodes: - *page_title_with_back - *login_id_mobile - *prompt_mobile + - *captcha_box - <<: *submit_button id: submit_uin label: Get OTP prompts: - - inputs: - - ref: username_input - identifier: username - type: PHONE_INPUT - required: true + - inputs: *otp_inputs action: ref: submit_uin nextNode: send_mosip_otp @@ -389,15 +423,12 @@ nodes: - *page_title_with_back - *login_id_email - *prompt_email + - *captcha_box - <<: *submit_button id: submit_uin label: Get OTP prompts: - - inputs: - - ref: username_input - identifier: username - type: EMAIL_INPUT - required: true + - inputs: *otp_inputs action: ref: submit_uin nextNode: send_mosip_otp @@ -425,15 +456,12 @@ nodes: - *page_title_with_back - *login_id_nrc - *prompt_nrc + - *captcha_box - <<: *submit_button id: submit_uin label: Get OTP prompts: - - inputs: - - ref: username_input - identifier: username - type: TEXT_INPUT - required: true + - inputs: *otp_inputs action: ref: submit_uin nextNode: send_mosip_otp @@ -532,19 +560,12 @@ nodes: - *login_id_uin - *prompt_uin - *prompt_password + - *captcha_box - <<: *submit_button id: password_authenticate label: Login prompts: - - inputs: - - ref: username_input - identifier: username - type: TEXT_INPUT - required: true - - ref: password_input - identifier: password - type: PASSWORD_INPUT - required: true + - inputs: *password_inputs action: ref: password_authenticate nextNode: basic_cred_auth @@ -575,19 +596,12 @@ nodes: - *login_id_mobile - *prompt_mobile - *prompt_password + - *captcha_box - <<: *submit_button id: password_authenticate label: Login prompts: - - inputs: - - ref: username_input - identifier: username - type: TEXT_INPUT - required: true - - ref: password_input - identifier: password - type: PASSWORD_INPUT - required: true + - inputs: *password_inputs action: ref: password_authenticate nextNode: basic_cred_auth @@ -618,19 +632,12 @@ nodes: - *login_id_email - *prompt_email - *prompt_password + - *captcha_box - <<: *submit_button id: password_authenticate label: Login prompts: - - inputs: - - ref: username_input - identifier: username - type: TEXT_INPUT - required: true - - ref: password_input - identifier: password - type: PASSWORD_INPUT - required: true + - inputs: *password_inputs action: ref: password_authenticate nextNode: basic_cred_auth @@ -661,19 +668,12 @@ nodes: - *login_id_nrc - *prompt_nrc - *prompt_password + - *captcha_box - <<: *submit_button id: password_authenticate label: Login prompts: - - inputs: - - ref: username_input - identifier: username - type: TEXT_INPUT - required: true - - ref: password_input - identifier: password - type: PASSWORD_INPUT - required: true + - inputs: *password_inputs action: ref: password_authenticate nextNode: basic_cred_auth @@ -703,6 +703,10 @@ nodes: identifier: password type: PASSWORD_INPUT required: true + - ref: CAPTCHA_BOX + identifier: captcha + type: PASSWORD_INPUT + required: true onSuccess: authorization_check - id: prompt_uin_bio @@ -905,6 +909,7 @@ nodes: required: true placeholder: Select your date of birth dateFormat: "yyyy-MM-dd" + - *captcha_box - <<: *submit_button id: submit_kbi_details label: Login @@ -922,6 +927,10 @@ nodes: identifier: dob type: DATE_INPUT required: true + - ref: CAPTCHA_BOX + identifier: captcha + type: CUSTOM + required: true action: ref: submit_kbi_details nextNode: basic_kbi_auth @@ -946,6 +955,10 @@ nodes: identifier: dob type: PASSWORD_INPUT required: true + - ref: CAPTCHA_BOX + identifier: captcha + type: PASSWORD_INPUT + required: true onSuccess: authorization_check - id: authorization_check diff --git a/esignet-service/internal/engine/flow_provider.go b/esignet-service/internal/engine/flow_provider.go index 49c46120e..7b9a83041 100644 --- a/esignet-service/internal/engine/flow_provider.go +++ b/esignet-service/internal/engine/flow_provider.go @@ -46,7 +46,8 @@ func (p *flowProvider) parseFlowDefinition() (*providers.CompleteFlowDefinition, return nil, shared.FileNotFoundError } var flowDef providers.CompleteFlowDefinition - err = yaml.Unmarshal(data, &flowDef) + expanded := os.ExpandEnv(string(data)) + err = yaml.Unmarshal([]byte(expanded), &flowDef) if err != nil { applog.GetLogger().Warn("failed to parse flow definition file", applog.String("flowId", p.cfg.AuthFlowID), applog.Error(err)) return nil, shared.FileUnmarshallError diff --git a/oidc-ui/package-lock.json b/oidc-ui/package-lock.json index 517d8cfd3..5eb868ea1 100644 --- a/oidc-ui/package-lock.json +++ b/oidc-ui/package-lock.json @@ -8,13 +8,16 @@ "name": "oidc-ui", "version": "0.0.0", "dependencies": { + "@hcaptcha/react-hcaptcha": "^2.0.2", + "@marsidev/react-turnstile": "^1.5.3", "@mosip/secure-biometric-interface-integrator": "^0.1.0", "@tanstack/react-query": "^5.100.9", - "@thunderid/react": "0.6.0", + "@thunderid/react": "^0.10.1", "axios": "^1.16.0", "react": "^19.2.5", "react-detect-offline": "^2.4.5", "react-dom": "^19.2.5", + "react-google-recaptcha": "^3.1.0", "react-router-dom": "^7.15.0" }, "devDependencies": { @@ -28,7 +31,9 @@ "@types/react": "^19.2.14", "@types/react-detect-offline": "^2.4.6", "@types/react-dom": "^19.2.3", + "@types/react-google-recaptcha": "^2.1.9", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.10", "autoprefixer": "^10.5.0", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", @@ -43,7 +48,7 @@ "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10", - "vitest": "^4.1.5" + "vitest": "^4.1.10" } }, "node_modules/@adobe/css-tools": { @@ -350,6 +355,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -778,22 +793,22 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react": { @@ -812,12 +827,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.6" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -825,9 +840,15 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@hcaptcha/react-hcaptcha": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-2.0.2.tgz", + "integrity": "sha512-VbuH6VJ6m3BHmVBHs0fL9t+suZd7PQEqCzqL2BiUbBvbHI3XfvSgdiug2QiEPN8zskbPTIV/FfGPF53JCckrow==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -942,6 +963,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@marsidev/react-turnstile": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.5.3.tgz", + "integrity": "sha512-8Dij2jiNGNczq1U4EKpO4do2XepcTPxSMc2ZzvHndO+gcp68tvMULm27z2P99rGkdB89hc3452NZeu2Rti4g6A==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.2 || ^18.0.0 || ^19.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" + } + }, "node_modules/@mosip/secure-biometric-interface-integrator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mosip/secure-biometric-interface-integrator/-/secure-biometric-interface-integrator-0.1.0.tgz", @@ -1642,17 +1673,17 @@ } }, "node_modules/@thunderid/browser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@thunderid/browser/-/browser-0.6.0.tgz", - "integrity": "sha512-rcnPb57cJA2X1tDIkQdnnXvTCIndrLj04Fx/IdgO+Z2twEKg0wrPZ4jpGPM6lmrjbxXhR+V+OvWt8+OAjku3Lw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@thunderid/browser/-/browser-0.10.1.tgz", + "integrity": "sha512-xFLoKWn9tUUBdOWf4dCFJ5Alt+dD5NHC5Cvh5RlI/RpuNEJk7mc8WzANAlJlpIGBoIcQ4PzjMgi7PZqzZ/LlDQ==", "license": "Apache-2.0", "dependencies": { - "@thunderid/javascript": "^0.6.0", + "@thunderid/javascript": "^0.10.1", "base64url": "3.0.1", "buffer": "6.0.3", "core-js": "3.42.0", "fast-sha256": "1.3.0", - "jose": "5.2.0", + "jose": "6.2.3", "process": "0.11.10", "randombytes": "2.1.0", "stream-browserify": "3.0.0", @@ -1660,42 +1691,42 @@ } }, "node_modules/@thunderid/browser/node_modules/jose": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.0.tgz", - "integrity": "sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, "node_modules/@thunderid/javascript": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@thunderid/javascript/-/javascript-0.6.0.tgz", - "integrity": "sha512-bIH0pbj1yK5iUC9s23fCH7y/OqWNpsg5morzzS8FICTywU5BHuIl6+WhAxy2TO4El40clZjivJe+mXXiMJ0XBw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@thunderid/javascript/-/javascript-0.10.1.tgz", + "integrity": "sha512-pjTzx+nOgBZAw4Q689MXTsRu+KFEdiKKyhq4FCRWzi5TQEVU9vxIuRgp1B7gQBbhtTXN5IEv7WGkbAQtH/6KTg==", "license": "Apache-2.0", "dependencies": { - "jose": "5.2.0", + "jose": "6.2.3", "tslib": "2.8.1" } }, "node_modules/@thunderid/javascript/node_modules/jose": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.0.tgz", - "integrity": "sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, "node_modules/@thunderid/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@thunderid/react/-/react-0.6.0.tgz", - "integrity": "sha512-w6Tj/Of+9SC2085afAfEgFSpX4l91QdGzHLonWZxTR9xKwOqGMSV2wlK/w2Lc8o8ISdq/nHG4P9AeG1vSPDOsQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@thunderid/react/-/react-0.10.1.tgz", + "integrity": "sha512-Iu/W/YKdAFGnJ/8RohmoVeqNvNd07g6ME7AJ2g5nghJJwIgIceti8tLinl0DMOlYsCGDmtVglfxrc4Dc/h+fNg==", "license": "Apache-2.0", "dependencies": { "@emotion/css": "11.13.5", "@floating-ui/react": "0.27.12", - "@thunderid/browser": "^0.6.0", + "@thunderid/browser": "^0.10.1", "dompurify": "3.4.11", "tslib": "2.8.1" }, @@ -1807,6 +1838,16 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-google-recaptcha": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz", + "integrity": "sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2083,17 +2124,48 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2102,13 +2174,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.8", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2129,9 +2201,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2142,13 +2214,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.8", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -2156,14 +2228,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2172,9 +2244,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -2182,13 +2254,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2298,6 +2370,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3376,6 +3467,16 @@ "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3432,6 +3533,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -3445,6 +3561,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3611,6 +3734,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4050,7 +4212,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4089,6 +4250,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4198,7 +4400,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4459,7 +4660,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -4471,7 +4671,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/proxy-from-env": { @@ -4511,6 +4710,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-async-script": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz", + "integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, "node_modules/react-detect-offline": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/react-detect-offline/-/react-detect-offline-2.4.5.tgz", @@ -4529,6 +4741,19 @@ "react": "^19.2.7" } }, + "node_modules/react-google-recaptcha": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", + "integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.0", + "react-async-script": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -4832,6 +5057,19 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5181,19 +5419,19 @@ } }, "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -5221,12 +5459,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/oidc-ui/package.json b/oidc-ui/package.json index c6c589ae3..d9aa23865 100644 --- a/oidc-ui/package.json +++ b/oidc-ui/package.json @@ -14,13 +14,16 @@ "typecheck": "tsc -b" }, "dependencies": { + "@hcaptcha/react-hcaptcha": "^2.0.2", + "@marsidev/react-turnstile": "^1.5.3", "@mosip/secure-biometric-interface-integrator": "^0.1.0", "@tanstack/react-query": "^5.100.9", - "@thunderid/react": "0.6.0", + "@thunderid/react": "^0.10.1", "axios": "^1.16.0", "react": "^19.2.5", "react-detect-offline": "^2.4.5", "react-dom": "^19.2.5", + "react-google-recaptcha": "^3.1.0", "react-router-dom": "^7.15.0" }, "devDependencies": { @@ -34,7 +37,9 @@ "@types/react": "^19.2.14", "@types/react-detect-offline": "^2.4.6", "@types/react-dom": "^19.2.3", + "@types/react-google-recaptcha": "^2.1.9", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.10", "autoprefixer": "^10.5.0", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", @@ -49,6 +54,6 @@ "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10", - "vitest": "^4.1.5" + "vitest": "^4.1.10" } } diff --git a/oidc-ui/src/__tests__/pages/PageNotFoundPage.test.tsx b/oidc-ui/src/__tests__/pages/PageNotFoundPage.test.tsx index 41a434f17..ca5501203 100644 --- a/oidc-ui/src/__tests__/pages/PageNotFoundPage.test.tsx +++ b/oidc-ui/src/__tests__/pages/PageNotFoundPage.test.tsx @@ -1,22 +1,22 @@ -import { describe, it, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import PageNotFoundPage from '../../pages/PageNotFoundPage'; +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import PageNotFoundPage from "../../pages/PageNotFoundPage"; -describe('PageNotFoundPage', () => { - it('renders the under construction image', () => { +describe("PageNotFoundPage", () => { + it("renders the under construction image", () => { render(); - expect(screen.getByAltText('page_not_found')).toBeDefined(); + expect(screen.getByAltText("Page not found")).toBeDefined(); }); - it('renders the page not found heading', () => { + it("renders the page not found heading", () => { render(); - expect(screen.getByText('Page Not Found')).toBeDefined(); + expect(screen.getByText("Page Not Found")).toBeDefined(); }); - it('has the correct CSS classes', () => { + it("has the correct CSS classes", () => { const { container } = render(); const card = container.firstElementChild; - expect(card?.className).toContain('multipurpose-login-card'); - expect(card?.className).toContain('section-background'); + expect(card?.className).toContain("multipurpose-login-card"); + expect(card?.className).toContain("section-background"); }); }); diff --git a/oidc-ui/src/components/CaptchaComponent/CaptchaComponent.tsx b/oidc-ui/src/components/CaptchaComponent/CaptchaComponent.tsx new file mode 100644 index 000000000..a023b141d --- /dev/null +++ b/oidc-ui/src/components/CaptchaComponent/CaptchaComponent.tsx @@ -0,0 +1,78 @@ +import { FormControl, validateFieldValue } from "@thunderid/react"; +import GoogleReCaptcha from "./GoogleReCaptcha"; +import CloudflareTurnstile from "./CloudflareTurnstile"; +import HCaptcha from "./HCaptcha"; +import type { CaptchaComponentProps } from "./CaptchaModel"; + +export default function CaptchaComponent({ + component, + context, +}: CaptchaComponentProps) { + const fieldRef = component.ref ?? component.id; + const { formValues, formErrors, touchedFields } = context; + const isTouched = touchedFields[fieldRef] || false; + const values = formValues[fieldRef]; + const { provider } = component.captcha ?? {}; + + const validationError = validateFieldValue( + values, + "TEXT" as any, + component.required, + isTouched, + ); + + const error = formErrors[fieldRef] || validationError || undefined; + + /** + * Setting the captcha in the form + * @param token captcha token got from captcha component + */ + const handleSuccess = (token: string | null) => { + context.onInputChange(fieldRef, token ?? ""); + }; + + /** + * Reset the captcha value as soon as it is expire + */ + const handleExpire = () => { + context.onInputChange(fieldRef, ""); + }; + + /** + * Reset the captcha values as soon as it encountered error + */ + const handleError = () => { + context.onInputChange(fieldRef, ""); + }; + + const captchaProps = { + captcha: component.captcha, + handleSuccess, + handleError, + handleExpire, + }; + + let providerElement = null; + + if (provider === "google-recaptcha") { + providerElement = ; + } else if (provider === "cloudflare-turnstile") { + providerElement = ; + } else if (provider === "hcaptcha") { + providerElement = ; + } else { + providerElement = ( + CAPTCHA provider configuration is invalid. + ); + context.onInputChange(fieldRef, ""); + context.formErrors[fieldRef] = "CAPTCHA provider configuration is invalid."; + } + + return ( + <> + {providerElement ? ( + {providerElement} + ) : null} + + ); +} diff --git a/oidc-ui/src/components/CaptchaComponent/CaptchaModel.tsx b/oidc-ui/src/components/CaptchaComponent/CaptchaModel.tsx new file mode 100644 index 000000000..debc9673a --- /dev/null +++ b/oidc-ui/src/components/CaptchaComponent/CaptchaModel.tsx @@ -0,0 +1,32 @@ +import type { + ComponentRenderContext, + EmbeddedFlowComponent, +} from "@thunderid/react"; + +export type CaptchaProvider = + | "google-recaptcha" + | "cloudflare-turnstile" + | "hcaptcha"; + +export interface CaptchaComponent { + provider?: CaptchaProvider; + siteKey?: string; + theme?: "light" | "dark"; + size?: string; +} + +export interface CaptchaFlowComponent extends EmbeddedFlowComponent { + captcha?: CaptchaComponent; +} + +export interface CaptchaComponentProps { + component: CaptchaFlowComponent; + context: ComponentRenderContext; +} + +export interface CaptchaProps { + captcha: CaptchaComponent | undefined; + handleSuccess: (token: string | null) => void; + handleError: () => void; + handleExpire: () => void; +} diff --git a/oidc-ui/src/components/CaptchaComponent/CaptchaRenderer.tsx b/oidc-ui/src/components/CaptchaComponent/CaptchaRenderer.tsx new file mode 100644 index 000000000..c4a6cd07a --- /dev/null +++ b/oidc-ui/src/components/CaptchaComponent/CaptchaRenderer.tsx @@ -0,0 +1,18 @@ +import type { ComponentRenderContext } from "@thunderid/react"; +import CaptchaComponent from "./CaptchaComponent"; +import type { CaptchaFlowComponent } from "./CaptchaModel"; + +export default function CaptchaRenderer( + component: CaptchaFlowComponent, + context: ComponentRenderContext, +) { + return ( +
+ +
+ ); +} diff --git a/oidc-ui/src/components/CaptchaComponent/CloudflareTurnstile.tsx b/oidc-ui/src/components/CaptchaComponent/CloudflareTurnstile.tsx new file mode 100644 index 000000000..9f29d3430 --- /dev/null +++ b/oidc-ui/src/components/CaptchaComponent/CloudflareTurnstile.tsx @@ -0,0 +1,24 @@ +import { Turnstile } from "@marsidev/react-turnstile"; +import type { CaptchaProps } from "./CaptchaModel"; + +export default function CloudflareTurnstile({ + captcha, + handleSuccess, + handleError, + handleExpire, +}: CaptchaProps) { + const { siteKey, theme, size } = captcha ?? {}; + + return ( + + ); +} diff --git a/oidc-ui/src/components/CaptchaComponent/GoogleReCaptcha.tsx b/oidc-ui/src/components/CaptchaComponent/GoogleReCaptcha.tsx new file mode 100644 index 000000000..496364591 --- /dev/null +++ b/oidc-ui/src/components/CaptchaComponent/GoogleReCaptcha.tsx @@ -0,0 +1,22 @@ +import ReCAPTCHA from "react-google-recaptcha"; +import type { CaptchaProps } from "./CaptchaModel"; + +export default function GoogleReCaptcha({ + captcha, + handleSuccess, + handleError, + handleExpire, +}: CaptchaProps) { + const { siteKey, theme, size } = captcha ?? {}; + + return ( + + ); +} diff --git a/oidc-ui/src/components/CaptchaComponent/HCaptcha.tsx b/oidc-ui/src/components/CaptchaComponent/HCaptcha.tsx new file mode 100644 index 000000000..ba88d7e1a --- /dev/null +++ b/oidc-ui/src/components/CaptchaComponent/HCaptcha.tsx @@ -0,0 +1,22 @@ +import HCaptchaLib from "@hcaptcha/react-hcaptcha"; +import type { CaptchaProps } from "./CaptchaModel"; + +export default function HCaptcha({ + captcha, + handleSuccess, + handleError, + handleExpire, +}: CaptchaProps) { + const { siteKey, theme, size } = captcha ?? {}; + + return ( + + ); +} diff --git a/oidc-ui/src/components/index.tsx b/oidc-ui/src/components/index.tsx index 08b931ed1..c8469b2eb 100644 --- a/oidc-ui/src/components/index.tsx +++ b/oidc-ui/src/components/index.tsx @@ -1,5 +1,18 @@ import SbiCustomRenderer from "./SbiComponent/SbiCustomRenderer"; import ResendOtpRenderer from "./ResendOtpComponent/ResendOtpRenderer"; import BackButtonRenderer from "./BackButtonComponent/BackButtonRenderer"; +import CaptchaRenderer from "./CaptchaComponent/CaptchaRenderer"; -export { SbiCustomRenderer, ResendOtpRenderer, BackButtonRenderer }; +export { + SbiCustomRenderer, + ResendOtpRenderer, + BackButtonRenderer, + CaptchaRenderer, +}; + +export type { + CaptchaFlowComponent, + CaptchaProvider, + CaptchaComponentProps, + CaptchaProps, +} from "./CaptchaComponent/CaptchaModel"; diff --git a/oidc-ui/src/main.tsx b/oidc-ui/src/main.tsx index 0227dacb5..94d11a54c 100644 --- a/oidc-ui/src/main.tsx +++ b/oidc-ui/src/main.tsx @@ -6,6 +6,7 @@ import { SbiCustomRenderer, ResendOtpRenderer, BackButtonRenderer, + CaptchaRenderer, } from "./components"; // getting applicationId from query param to pass it to ThunderIDProvider @@ -36,6 +37,7 @@ createRoot(document.getElementById("root")!).render( SBI_ID: SbiCustomRenderer, RESEND_OTP: ResendOtpRenderer, BACK_BUTTON: BackButtonRenderer, + CAPTCHA_BOX: CaptchaRenderer, }, }, }} diff --git a/oidc-ui/src/pages/LoginPage.tsx b/oidc-ui/src/pages/LoginPage.tsx index c723b1e34..68f7c3a13 100644 --- a/oidc-ui/src/pages/LoginPage.tsx +++ b/oidc-ui/src/pages/LoginPage.tsx @@ -42,7 +42,11 @@ export default function LoginPage() { "!rounded-lg w-auto sm:w-3/6 lg:max-w-sm md:z-10 md:m-auto py-4" } > - {isLoading ? : } + {isLoading ? ( + + ) : ( + + )} ); }