Skip to content

Commit

Permalink
Add hCaptcha support (mastodon#25019)
Browse files Browse the repository at this point in the history
  • Loading branch information
ClearlyClaire authored May 16, 2023
1 parent e604147 commit bec6a1c
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,5 @@ gem 'cocoon', '~> 1.2'

gem 'net-http', '~> 0.3.2'
gem 'rubyzip', '~> 2.3'

gem 'hcaptcha', '~> 7.1'
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ GEM
sysexits (~> 1.1)
hashdiff (1.0.1)
hashie (5.0.0)
hcaptcha (7.1.0)
json
highline (2.1.0)
hiredis (0.6.3)
hkdf (0.3.0)
Expand Down Expand Up @@ -806,6 +808,7 @@ DEPENDENCIES
fuubar (~> 2.5)
haml-rails (~> 2.0)
haml_lint
hcaptcha (~> 7.1)
hiredis (~> 0.6)
htmlentities (~> 4.3)
http (~> 5.1)
Expand Down
42 changes: 42 additions & 0 deletions app/controllers/auth/confirmations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,63 @@
# frozen_string_literal: true

class Auth::ConfirmationsController < Devise::ConfirmationsController
include CaptchaConcern

layout 'auth'

before_action :set_body_classes
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
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!

def show
old_session_values = session.to_hash
reset_session
session.update old_session_values.except('session_id')

super
end

def new
super

resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
end

def confirm_captcha
check_captcha! do |message|
flash.now[:alert] = message
render :captcha
return
end

show
end

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!
if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
Expand Down
59 changes: 59 additions & 0 deletions app/controllers/concerns/captcha_concern.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions app/helpers/admin/settings_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# frozen_string_literal: true

module Admin::SettingsHelper
def captcha_available?
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
end
end
8 changes: 8 additions & 0 deletions app/javascript/styles/mastodon/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ code {
line-height: 22px;
color: $secondary-text-color;
margin-bottom: 30px;

a {
color: $highlight-text-color;
}
}

.rules-list {
Expand Down Expand Up @@ -1039,6 +1043,10 @@ code {
}
}

.simple_form .h-captcha {
text-align: center;
}

.permissions-list {
&__item {
padding: 15px;
Expand Down
2 changes: 2 additions & 0 deletions app/models/form/admin_settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Form::AdminSettings
content_cache_retention_period
backups_retention_period
status_page_url
captcha_enabled
).freeze

INTEGER_KEYS = %i(
Expand All @@ -52,6 +53,7 @@ class Form::AdminSettings
trendable_by_default
noindex
require_invite_text
captcha_enabled
).freeze

UPLOAD_KEYS = %i(
Expand Down
4 changes: 4 additions & 0 deletions app/views/admin/settings/registrations/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
.fields-row__column.fields-row__column-6.fields-group
= 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
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 }

Expand Down
15 changes: 15 additions & 0 deletions app/views/auth/confirmations/captcha.html.haml
Original file line number Diff line number Diff line change
@@ -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')
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,9 @@ en:
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.
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:
preamble: Control how user-generated content is stored in Mastodon.
title: Content retention
Expand Down Expand Up @@ -979,6 +982,9 @@ en:
your_token: Your access token
auth:
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
confirmations:
wrong_email_hint: If that e-mail address is not correct, you can change it in account settings.
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
resource :setup, only: [:show, :update], controller: :setup
resource :challenge, only: [:create], controller: :challenges
get 'sessions/security_key_options', to: 'sessions#webauthn_options'
post 'captcha_confirmation', to: 'confirmations#confirm_captcha', as: :captcha_confirmation
end
end

Expand Down
1 change: 1 addition & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defaults: &defaults
show_domain_blocks_rationale: 'disabled'
require_invite_text: false
backups_retention_period: 7
captcha_enabled: false

development:
<<: *defaults
Expand Down

0 comments on commit bec6a1c

Please sign in to comment.