~cytrogen/masto-fe

bec6a1cad4c509c53deb378c7ba984ba7e2de5a9 — Claire 2 years ago e604147
Add hCaptcha support (#25019)

M Gemfile => Gemfile +2 -0
@@ 160,3 160,5 @@ gem 'cocoon', '~> 1.2'

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

gem 'hcaptcha', '~> 7.1'

M Gemfile.lock => Gemfile.lock +3 -0
@@ 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)


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

M app/controllers/auth/confirmations_controller.rb => app/controllers/auth/confirmations_controller.rb +42 -0
@@ 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)

A app/controllers/concerns/captcha_concern.rb => app/controllers/concerns/captcha_concern.rb +59 -0
@@ 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

M app/helpers/admin/settings_helper.rb => app/helpers/admin/settings_helper.rb +3 -0
@@ 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

M app/javascript/styles/mastodon/forms.scss => app/javascript/styles/mastodon/forms.scss +8 -0
@@ 136,6 136,10 @@ code {
    line-height: 22px;
    color: $secondary-text-color;
    margin-bottom: 30px;

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

  .rules-list {


@@ 1039,6 1043,10 @@ code {
  }
}

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

.permissions-list {
  &__item {
    padding: 15px;

M app/models/form/admin_settings.rb => app/models/form/admin_settings.rb +2 -0
@@ 33,6 33,7 @@ class Form::AdminSettings
    content_cache_retention_period
    backups_retention_period
    status_page_url
    captcha_enabled
  ).freeze

  INTEGER_KEYS = %i(


@@ 52,6 53,7 @@ class Form::AdminSettings
    trendable_by_default
    noindex
    require_invite_text
    captcha_enabled
  ).freeze

  UPLOAD_KEYS = %i(

M app/views/admin/settings/registrations/show.html.haml => app/views/admin/settings/registrations/show.html.haml +4 -0
@@ 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 }


A app/views/auth/confirmations/captcha.html.haml => app/views/auth/confirmations/captcha.html.haml +15 -0
@@ 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')

M config/locales/en.yml => config/locales/en.yml +6 -0
@@ 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


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

M config/routes.rb => config/routes.rb +1 -0
@@ 71,6 71,7 @@ Rails.application.routes.draw do
      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


M config/settings.yml => config/settings.yml +1 -0
@@ 37,6 37,7 @@ defaults: &defaults
  show_domain_blocks_rationale: 'disabled'
  require_invite_text: false
  backups_retention_period: 7
  captcha_enabled: false

development:
  <<: *defaults