From 1b493c9fee954b5bd4c4b00f9f945a5d97e2d699 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Jan 2022 19:06:19 +0100 Subject: Add optional hCaptcha support Fixes #1649 This requires setting `HCAPTCHA_SECRET_KEY` and `HCAPTCHA_SITE_KEY`, then enabling the admin setting at `/admin/settings/edit#form_admin_settings_captcha_enabled` Subsequently, a hCaptcha widget will be displayed on `/about` and `/auth/sign_up` unless: - the user is already signed-up already - the user has used an invite link - the user has already solved the captcha (and registration failed for another reason) The Content-Security-Policy headers are altered automatically to allow the third-party hCaptcha scripts on `/about` and `/auth/sign_up` following the same rules as above. --- app/controllers/about_controller.rb | 2 + app/controllers/api/v1/accounts_controller.rb | 4 +- app/controllers/auth/registrations_controller.rb | 17 ++++++ app/controllers/concerns/captcha_concern.rb | 66 ++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 app/controllers/concerns/captcha_concern.rb (limited to 'app/controllers') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 620c0ff78..5a35dbbcb 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,6 +2,7 @@ class AboutController < ApplicationController include RegistrationSpamConcern + include CaptchaConcern before_action :set_pack @@ -12,6 +13,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter before_action :set_expires_in, only: [:more, :terms] before_action :set_registration_form_time, only: :show + before_action :extend_csp_for_captcha!, only: :show skip_before_action :require_functional!, only: [:more, :terms] diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 5c47158e0..8916c3f96 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::AccountsController < Api::BaseController + include CaptchaConcern + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers] before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] @@ -83,7 +85,7 @@ class Api::V1::AccountsController < Api::BaseController end def check_enabled_registrations - forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? + forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? || captcha_enabled? end def allowed_registrations? diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 6b1f3fa82..3c9b38a4b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -2,6 +2,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController include RegistrationSpamConcern + include CaptchaConcern layout :determine_layout @@ -15,6 +16,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :require_not_suspended!, only: [:update] before_action :set_cache_headers, only: [:edit, :update] before_action :set_registration_form_time, only: :new + before_action :extend_csp_for_captcha!, only: [:new, :create] + before_action :check_captcha!, only: :create skip_before_action :require_functional!, only: [:edit, :update] @@ -135,4 +138,18 @@ class Auth::RegistrationsController < Devise::RegistrationsController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def sign_up(resource_name, resource) + clear_captcha! + super + end + + def check_captcha! + super do |error| + build_resource(sign_up_params) + resource.validate + resource.errors.add(:base, error) + respond_with resource + end + end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb new file mode 100644 index 000000000..5a23e59e3 --- /dev/null +++ b/app/controllers/concerns/captcha_concern.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module CaptchaConcern + extend ActiveSupport::Concern + include Hcaptcha::Adapters::ViewMethods + + CAPTCHA_TIMEOUT = 2.hours.freeze + + included do + helper_method :render_captcha_if_needed + end + + def captcha_available? + ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? + end + + def captcha_enabled? + captcha_available? && Setting.captcha_enabled + end + + def captcha_recently_passed? + session[:captcha_passed_at].present? && session[:captcha_passed_at] >= CAPTCHA_TIMEOUT.ago + end + + def captcha_required? + captcha_enabled? && !current_user && !(@invite.present? && @invite.valid_for_use? && !@invite.max_uses.nil?) && !captcha_recently_passed? + end + + def clear_captcha! + session.delete(:captcha_passed_at) + end + + def check_captcha! + return true unless captcha_required? + + if verify_hcaptcha + session[:captcha_passed_at] = Time.now.utc + return true + else + if block_given? + message = flash[:hcaptcha_error] + flash.delete(:hcaptcha_error) + yield message + end + return false + end + end + + def extend_csp_for_captcha! + policy = request.content_security_policy + return unless captcha_required? && policy.present? + + %w(script_src frame_src style_src connect_src).each do |directive| + values = policy.send(directive) + values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') + values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') + policy.send(directive, *values) + end + end + + def render_captcha_if_needed + return unless captcha_required? + + hcaptcha_tags + end +end -- cgit