feat: Add hCaptcha for public forms (#4017)

- added hCaptcha based verification for chatwoot signups

Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S 2022-02-18 20:02:50 +05:30 committed by GitHub
parent 80d83b401c
commit ea44a32758
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 123 additions and 7 deletions

View file

@ -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,

View file

@ -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

View file

@ -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);

View file

@ -75,8 +75,14 @@
"
@blur="$v.credentials.confirmPassword.$touch"
/>
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
<vue-hcaptcha
:sitekey="globalConfig.hCaptchaSiteKey"
@verify="onRecaptchaVerified"
/>
</div>
<woot-submit-button
:disabled="isSignupInProgress"
:disabled="isSignupInProgress || !hasAValidCaptcha"
:button-text="$t('REGISTER.SUBMIT')"
:loading="isSignupInProgress"
button-class="large expanded"
@ -107,8 +113,11 @@ import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import alertMixin from 'shared/mixins/alertMixin';
import { DEFAULT_REDIRECT_URL } from '../../constants';
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
export default {
components: {
VueHcaptcha,
},
mixins: [globalConfigMixin, alertMixin],
data() {
return {
@ -118,6 +127,7 @@ export default {
email: '',
password: '',
confirmPassword: '',
hCaptchaClientResponse: '',
},
isSignupInProgress: false,
error: '',
@ -153,9 +163,7 @@ export default {
},
},
computed: {
...mapGetters({
globalConfig: 'globalConfig/get',
}),
...mapGetters({ globalConfig: 'globalConfig/get' }),
termsLink() {
return this.$t('REGISTER.TERMS_ACCEPT')
.replace('https://www.chatwoot.com/terms', this.globalConfig.termsURL)
@ -164,6 +172,12 @@ export default {
this.globalConfig.privacyURL
);
},
hasAValidCaptcha() {
if (this.globalConfig.hCaptchaSiteKey) {
return !!this.credentials.hCaptchaClientResponse;
}
return true;
},
},
methods: {
async submit() {
@ -187,6 +201,9 @@ export default {
this.isSignupInProgress = false;
}
},
onRecaptchaVerified(token) {
this.credentials.hCaptchaClientResponse = token;
},
},
};
</script>
@ -234,5 +251,9 @@ export default {
text-align: center;
margin: var(--space-normal) 0 0 0;
}
.h-captcha--box {
margin-bottom: var(--space-one);
}
}
</style>

View file

@ -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,

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddHCaptchaKey < ActiveRecord::Migration[6.1]
def change
ConfigLoader.new.process
end
end

View file

@ -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"

25
lib/chatwoot_captcha.rb Normal file
View file

@ -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

View file

@ -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",

View file

@ -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)

View file

@ -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

View file

@ -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"