Add hCaptcha support (#25019)

This commit is contained in:
Claire 2023-05-16 23:27:35 +02:00 committed by GitHub
parent e60414792d
commit bec6a1cad4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 146 additions and 0 deletions

View file

@ -160,3 +160,5 @@ gem 'cocoon', '~> 1.2'
gem 'net-http', '~> 0.3.2' gem 'net-http', '~> 0.3.2'
gem 'rubyzip', '~> 2.3' gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1'

View file

@ -312,6 +312,8 @@ GEM
sysexits (~> 1.1) sysexits (~> 1.1)
hashdiff (1.0.1) hashdiff (1.0.1)
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0)
json
highline (2.1.0) highline (2.1.0)
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
@ -806,6 +808,7 @@ DEPENDENCIES
fuubar (~> 2.5) fuubar (~> 2.5)
haml-rails (~> 2.0) haml-rails (~> 2.0)
haml_lint haml_lint
hcaptcha (~> 7.1)
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 5.1) http (~> 5.1)

View file

@ -1,21 +1,63 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::ConfirmationsController < Devise::ConfirmationsController class Auth::ConfirmationsController < Devise::ConfirmationsController
include CaptchaConcern
layout 'auth' layout 'auth'
before_action :set_body_classes before_action :set_body_classes
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
before_action :require_unconfirmed! before_action :require_unconfirmed!
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
before_action :require_captcha_if_needed!, only: [:show]
skip_before_action :require_functional! skip_before_action :require_functional!
def show
old_session_values = session.to_hash
reset_session
session.update old_session_values.except('session_id')
super
end
def new def new
super super
resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
end end
def confirm_captcha
check_captcha! do |message|
flash.now[:alert] = message
render :captcha
return
end
show
end
private private
def require_captcha_if_needed!
render :captcha if captcha_required?
end
def set_confirmation_user!
# We need to reimplement looking up the user because
# Devise::ConfirmationsController#show looks up and confirms in one
# step.
confirmation_token = params[:confirmation_token]
return if confirmation_token.nil?
@confirmation_user = User.find_first_by_auth_conditions(confirmation_token: confirmation_token)
end
def captcha_user_bypass?
return true if @confirmation_user.nil? || @confirmation_user.confirmed?
end
def require_unconfirmed! def require_unconfirmed!
if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
redirect_to(current_user.approved? ? root_path : edit_user_registration_path) redirect_to(current_user.approved? ? root_path : edit_user_registration_path)

View file

@ -0,0 +1,59 @@
# frozen_string_literal: true
module CaptchaConcern
extend ActiveSupport::Concern
include Hcaptcha::Adapters::ViewMethods
included do
helper_method :render_captcha
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_user_bypass?
false
end
def captcha_required?
captcha_enabled? && !captcha_user_bypass?
end
def check_captcha!
return true unless captcha_required?
if verify_hcaptcha
true
else
if block_given?
message = flash[:hcaptcha_error]
flash.delete(:hcaptcha_error)
yield message
end
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
return unless captcha_required?
hcaptcha_tags
end
end

View file

@ -1,4 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin::SettingsHelper module Admin::SettingsHelper
def captcha_available?
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
end
end end

View file

@ -136,6 +136,10 @@ code {
line-height: 22px; line-height: 22px;
color: $secondary-text-color; color: $secondary-text-color;
margin-bottom: 30px; margin-bottom: 30px;
a {
color: $highlight-text-color;
}
} }
.rules-list { .rules-list {
@ -1039,6 +1043,10 @@ code {
} }
} }
.simple_form .h-captcha {
text-align: center;
}
.permissions-list { .permissions-list {
&__item { &__item {
padding: 15px; padding: 15px;

View file

@ -33,6 +33,7 @@ class Form::AdminSettings
content_cache_retention_period content_cache_retention_period
backups_retention_period backups_retention_period
status_page_url status_page_url
captcha_enabled
).freeze ).freeze
INTEGER_KEYS = %i( INTEGER_KEYS = %i(
@ -52,6 +53,7 @@ class Form::AdminSettings
trendable_by_default trendable_by_default
noindex noindex
require_invite_text require_invite_text
captcha_enabled
).freeze ).freeze
UPLOAD_KEYS = %i( UPLOAD_KEYS = %i(

View file

@ -20,6 +20,10 @@
.fields-row__column.fields-row__column-6.fields-group .fields-row__column.fields-row__column-6.fields-group
= f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations? = f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations?
- if captcha_available?
.fields-group
= f.input :captcha_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.captcha_enabled.title'), hint: t('admin.settings.captcha_enabled.desc_html')
.fields-group .fields-group
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 } = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 }

View file

@ -0,0 +1,15 @@
- content_for :page_title do
= t('auth.captcha_confirmation.title')
= form_tag auth_captcha_confirmation_url, method: 'POST', class: 'simple_form' do
= render 'auth/shared/progress', stage: 'confirm'
= hidden_field_tag :confirmation_token, params[:confirmation_token]
%p.lead= t('auth.captcha_confirmation.hint_html')
.field-group
= render_captcha
.actions
%button.button= t('challenge.confirm')

View file

@ -731,6 +731,9 @@ en:
branding: branding:
preamble: Your server's branding differentiates it from other servers in the network. This information may be displayed across a variety of environments, such as Mastodon's web interface, native applications, in link previews on other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear, short and concise. preamble: Your server's branding differentiates it from other servers in the network. This information may be displayed across a variety of environments, such as Mastodon's web interface, native applications, in link previews on other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear, short and concise.
title: Branding title: Branding
captcha_enabled:
desc_html: This relies on external scripts from hCaptcha, which may be a security and privacy concern. In addition, <strong>this can make the registration process significantly less accessible to some (especially disabled) people</strong>. For these reasons, please consider alternative measures such as approval-based or invite-based registration.
title: Require new users to solve a CAPTCHA to confirm their account
content_retention: content_retention:
preamble: Control how user-generated content is stored in Mastodon. preamble: Control how user-generated content is stored in Mastodon.
title: Content retention title: Content retention
@ -979,6 +982,9 @@ en:
your_token: Your access token your_token: Your access token
auth: auth:
apply_for_account: Request an account apply_for_account: Request an account
captcha_confirmation:
hint_html: Just one more step! To confirm your account, this server requires you to solve a CAPTCHA. You can <a href="/about/more">contact the server administrator</a> if you have questions or need assistance with confirming your account.
title: User verification
change_password: Password change_password: Password
confirmations: confirmations:
wrong_email_hint: If that e-mail address is not correct, you can change it in account settings. wrong_email_hint: If that e-mail address is not correct, you can change it in account settings.

View file

@ -71,6 +71,7 @@ Rails.application.routes.draw do
resource :setup, only: [:show, :update], controller: :setup resource :setup, only: [:show, :update], controller: :setup
resource :challenge, only: [:create], controller: :challenges resource :challenge, only: [:create], controller: :challenges
get 'sessions/security_key_options', to: 'sessions#webauthn_options' get 'sessions/security_key_options', to: 'sessions#webauthn_options'
post 'captcha_confirmation', to: 'confirmations#confirm_captcha', as: :captcha_confirmation
end end
end end

View file

@ -37,6 +37,7 @@ defaults: &defaults
show_domain_blocks_rationale: 'disabled' show_domain_blocks_rationale: 'disabled'
require_invite_text: false require_invite_text: false
backups_retention_period: 7 backups_retention_period: 7
captcha_enabled: false
development: development:
<<: *defaults <<: *defaults