From ea44a327589aa1608c5141218017582192728132 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Fri, 18 Feb 2022 20:02:50 +0530 Subject: [PATCH] feat: Add hCaptcha for public forms (#4017) - added hCaptcha based verification for chatwoot signups Co-authored-by: Sojan --- app/controllers/api/v1/accounts_controller.rb | 5 +++ app/controllers/dashboard_controller.rb | 3 +- app/javascript/dashboard/api/auth.js | 1 + .../dashboard/routes/auth/Signup.vue | 31 ++++++++++++++++--- app/javascript/shared/store/globalConfig.js | 2 ++ config/installation_config.yml | 6 ++++ .../20220218120357_add_h_captcha_key.rb | 5 +++ db/schema.rb | 2 +- lib/chatwoot_captcha.rb | 25 +++++++++++++++ package.json | 1 + .../api/v1/accounts_controller_spec.rb | 19 ++++++++++++ spec/lib/chatwoot_captcha_spec.rb | 25 +++++++++++++++ yarn.lock | 5 +++ 13 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20220218120357_add_h_captcha_key.rb create mode 100644 lib/chatwoot_captcha.rb create mode 100644 spec/lib/chatwoot_captcha_spec.rb diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index f1755b921..e75d3f852 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception, only: [:create], raise: false before_action :check_signup_enabled, only: [:create] + before_action :validate_captcha, only: [:create] before_action :fetch_account, except: [:create] before_action :check_authorization, except: [:create] @@ -58,6 +59,10 @@ class Api::V1::AccountsController < Api::BaseController raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false' end + def validate_captcha + raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid? + end + def pundit_user { user: current_user, diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 52c6c87af..b76ad4015 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -26,7 +26,8 @@ class DashboardController < ActionController::Base 'API_CHANNEL_THUMBNAIL', 'ANALYTICS_TOKEN', 'ANALYTICS_HOST', - 'DIRECT_UPLOADS_ENABLED' + 'DIRECT_UPLOADS_ENABLED', + 'HCAPTCHA_SITE_KEY' ).merge(app_config) end diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 7079614c1..18ec9e811 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -30,6 +30,7 @@ export default { user_full_name: creds.fullName.trim(), email: creds.email, password: creds.password, + h_captcha_client_response: creds.hCaptchaClientResponse, }) .then(response => { setAuthCredentials(response); diff --git a/app/javascript/dashboard/routes/auth/Signup.vue b/app/javascript/dashboard/routes/auth/Signup.vue index c175e7b9e..36b63662d 100644 --- a/app/javascript/dashboard/routes/auth/Signup.vue +++ b/app/javascript/dashboard/routes/auth/Signup.vue @@ -75,8 +75,14 @@ " @blur="$v.credentials.confirmPassword.$touch" /> +
+ +
@@ -234,5 +251,9 @@ export default { text-align: center; margin: var(--space-normal) 0 0 0; } + + .h-captcha--box { + margin-bottom: var(--space-one); + } } diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js index 9082db4ac..eaf88fedc 100644 --- a/app/javascript/shared/store/globalConfig.js +++ b/app/javascript/shared/store/globalConfig.js @@ -7,6 +7,7 @@ const { CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard, DIRECT_UPLOADS_ENABLED: directUploadsEnabled, DISPLAY_MANIFEST: displayManifest, + HCAPTCHA_SITE_KEY: hCaptchaSiteKey, INSTALLATION_NAME: installationName, LOGO_THUMBNAIL: logoThumbnail, LOGO: logo, @@ -24,6 +25,7 @@ const state = { createNewAccountFromDashboard, directUploadsEnabled: directUploadsEnabled === 'true', displayManifest, + hCaptchaSiteKey, installationName, logo, logoThumbnail, diff --git a/config/installation_config.yml b/config/installation_config.yml index fedf4fc0e..5e22e1a4f 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -47,3 +47,9 @@ - name: DIRECT_UPLOADS_ENABLED value: false locked: false +- name: HCAPTCHA_SITE_KEY + value: + locked: false +- name: HCAPTCHA_SERVER_KEY + value: + locked: false diff --git a/db/migrate/20220218120357_add_h_captcha_key.rb b/db/migrate/20220218120357_add_h_captcha_key.rb new file mode 100644 index 000000000..02b06e5d3 --- /dev/null +++ b/db/migrate/20220218120357_add_h_captcha_key.rb @@ -0,0 +1,5 @@ +class AddHCaptchaKey < ActiveRecord::Migration[6.1] + def change + ConfigLoader.new.process + end +end diff --git a/db/schema.rb b/db/schema.rb index b1f6775b3..c1cd951ab 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_02_15_060751) do +ActiveRecord::Schema.define(version: 2022_02_18_120357) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" diff --git a/lib/chatwoot_captcha.rb b/lib/chatwoot_captcha.rb new file mode 100644 index 000000000..37e852868 --- /dev/null +++ b/lib/chatwoot_captcha.rb @@ -0,0 +1,25 @@ +class ChatwootCaptcha + def initialize(client_response) + @client_response = client_response + @server_key = GlobalConfigService.load('HCAPTCHA_SERVER_KEY', '') + end + + def valid? + return true if @server_key.blank? + return false if @client_response.blank? + + validate_client_response? + end + + def validate_client_response? + response = HTTParty.post('https://hcaptcha.com/siteverify', + body: { + response: @client_response, + secret: @server_key + }) + + return unless response.success? + + response.parsed_response['success'] + end +end diff --git a/package.json b/package.json index 2c36a7284..e75b12b81 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@braid/vue-formulate": "^2.5.2", "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517", "@chatwoot/utils": "^0.0.3", + "@hcaptcha/vue-hcaptcha": "^0.3.2", "@rails/actioncable": "6.1.3", "@rails/webpacker": "5.3.0", "@sentry/tracing": "^6.4.1", diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index e86834d88..a82bd515d 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -30,6 +30,25 @@ RSpec.describe 'Accounts API', type: :request do end end + it 'calls ChatwootCaptcha' do + with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do + captcha = double + allow(account_builder).to receive(:perform).and_return([user, account]) + allow(ChatwootCaptcha).to receive(:new).and_return(captcha) + allow(captcha).to receive(:valid?).and_return(true) + + params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!', + h_captcha_client_response: '123' } + + post api_v1_accounts_url, + params: params, + as: :json + + expect(ChatwootCaptcha).to have_received(:new).with('123') + expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') + end + end + it 'renders error response on invalid params' do with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do allow(account_builder).to receive(:perform).and_return(nil) diff --git a/spec/lib/chatwoot_captcha_spec.rb b/spec/lib/chatwoot_captcha_spec.rb new file mode 100644 index 000000000..3b35c872e --- /dev/null +++ b/spec/lib/chatwoot_captcha_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +describe ChatwootCaptcha do + it 'returns true if HCAPTCHA SERVER KEY is absent' do + expect(described_class.new('random_key').valid?).to eq(true) + end + + context 'when HCAPTCHA SERVER KEY is present' do + before do + create(:installation_config, { name: 'HCAPTCHA_SERVER_KEY', value: 'hcaptch_server_key' }) + end + + it 'returns false if client response is blank' do + expect(described_class.new('').valid?).to eq false + end + + it 'returns true if client response is valid' do + captcha_request = double + allow(HTTParty).to receive(:post).and_return(captcha_request) + allow(captcha_request).to receive(:success?).and_return(true) + allow(captcha_request).to receive(:parsed_response).and_return({ 'success' => true }) + expect(described_class.new('valid_response').valid?).to eq true + end + end +end diff --git a/yarn.lock b/yarn.lock index f296da282..6463d91ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1283,6 +1283,11 @@ postcss "7.0.32" purgecss "^2.3.0" +"@hcaptcha/vue-hcaptcha@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942" + integrity sha512-JiJsAJh+fSe+uf9N3ek7CKzX/r79+hx+rMPch+e2/h9+Ei3VyJtb2Dgk1DhG/dyUdrooPIzkNMr6gfo75Cn22g== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"