From 9f7d379c2a018c86671bfc157fe1f0cf4e31e25e Mon Sep 17 00:00:00 2001
From: Stan Hu <stanhu@gmail.com>
Date: Sun, 27 Dec 2015 09:03:06 -0800
Subject: [PATCH] Add support for Google reCAPTCHA in user registration to
 prevent spammers

---
 CHANGELOG                                     |  1 +
 Gemfile                                       |  3 +++
 Gemfile.lock                                  |  3 +++
 app/controllers/registrations_controller.rb   | 23 +++++++++++++++++++
 app/controllers/sessions_controller.rb        | 13 ++++++-----
 app/views/devise/shared/_signup_box.html.haml |  3 +++
 config/gitlab.yml.example                     |  6 +++++
 config/initializers/1_settings.rb             |  7 ++++++
 config/initializers/recaptcha.rb              |  6 +++++
 9 files changed, 59 insertions(+), 6 deletions(-)
 create mode 100644 config/initializers/recaptcha.rb

diff --git a/CHANGELOG b/CHANGELOG
index a20c3978a11..e54a6669080 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 Please view this file on the master branch, on stable branches it's out of date.
 
 v 8.4.0 (unreleased)
+  - Add support for Google reCAPTCHA in user registration to prevent spammers (Stan Hu)
   - Implement new UI for group page
   - Implement search inside emoji picker
   - Add API support for looking up a user by username (Stan Hu)
diff --git a/Gemfile b/Gemfile
index db54bf2f186..a63c84fe94b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -35,6 +35,9 @@ gem 'omniauth-twitter',       '~> 1.2.0'
 gem 'omniauth_crowd'
 gem 'rack-oauth2',            '~> 1.2.1'
 
+# reCAPTCHA protection
+gem 'recaptcha', require: 'recaptcha/rails'
+
 # Two-factor authentication
 gem 'devise-two-factor', '~> 2.0.0'
 gem 'rqrcode-rails3', '~> 0.1.7'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4f4b10c0fb7..664a02c4bab 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -566,6 +566,8 @@ GEM
       trollop
     rdoc (3.12.2)
       json (~> 1.4)
+    recaptcha (1.0.2)
+      json
     redcarpet (3.3.3)
     redis (3.2.2)
     redis-actionpack (4.0.1)
@@ -924,6 +926,7 @@ DEPENDENCIES
   raphael-rails (~> 2.1.2)
   rblineprof
   rdoc (~> 3.6)
+  recaptcha
   redcarpet (~> 3.3.3)
   redis-namespace
   redis-rails (~> 4.0.0)
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3b3dc86cb68..283831f8149 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,10 +1,21 @@
 class RegistrationsController < Devise::RegistrationsController
   before_action :signup_enabled?
+  include Recaptcha::Verify
 
   def new
     redirect_to(new_user_session_path)
   end
 
+  def create
+    if !Gitlab.config.recaptcha.enabled || verify_recaptcha
+      super
+    else
+      flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
+      flash.delete :recaptcha_error
+      render action: 'new'
+    end
+  end
+
   def destroy
     DeleteUserService.new(current_user).execute(current_user)
 
@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
   def sign_up_params
     params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
   end
+
+  def resource_name
+    :user
+  end
+
+  def resource
+    @resource ||= User.new
+  end
+
+  def devise_mapping
+    @devise_mapping ||= Devise.mappings[:user]
+  end
 end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1b60d3e27d0..da4b35d322b 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,5 +1,6 @@
 class SessionsController < Devise::SessionsController
   include AuthenticatesWithTwoFactor
+  include Recaptcha::ClientHelper
 
   prepend_before_action :authenticate_with_two_factor, only: [:create]
   prepend_before_action :store_redirect_path, only: [:new]
@@ -40,7 +41,7 @@ class SessionsController < Devise::SessionsController
       User.find(session[:otp_user_id])
     end
   end
-  
+
   def store_redirect_path
     redirect_path =
       if request.referer.present? && (params['redirect_to_referer'] == 'yes')
@@ -87,14 +88,14 @@ class SessionsController < Devise::SessionsController
     provider = Gitlab.config.omniauth.auto_sign_in_with_provider
     return unless provider.present?
 
-    # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is 
-    # registered or no alert at all. In case of another alert (such as a blocked user), it is safer  
+    # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
+    # registered or no alert at all. In case of another alert (such as a blocked user), it is safer
     # to do nothing to prevent redirection loops with certain Omniauth providers.
     return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated')
-    
+
     # Prevent alert from popping up on the first page shown after authentication.
-    flash[:alert] = nil 
-    
+    flash[:alert] = nil
+
     redirect_to user_omniauth_authorize_path(provider.to_sym)
   end
 
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 9dc6aeffd59..3c079d7e2f8 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -17,6 +17,9 @@
         = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
       .form-group.append-bottom-20#password-strength
         = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
+      %div
+      - if Gitlab.config.recaptcha.enabled
+        = recaptcha_tags
       %div
         = f.submit "Sign up", class: "btn-create btn"
 
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index db68b5512b8..79fc7423b31 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -346,6 +346,12 @@ production: &base
     # cas3:
     #   session_duration: 28800
 
+  # reCAPTCHA settings. See: http://www.google.com/recaptcha
+  recaptcha:
+    enabled: false
+    public_key: 'YOUR_PUBLIC_KEY'
+    private_key: 'YOUR_PRIVATE_KEY'
+
   # Shared file storage settings
   shared:
     # path: /mnt/gitlab # Default: shared
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 816cb0c02a9..0dc4838fec1 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -131,6 +131,13 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours
 Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
 Settings.omniauth.session_tickets['cas3'] = 'ticket'
 
+# ReCAPTCHA settings
+Settings['recaptcha'] ||= Settingslogic.new({})
+Settings.recaptcha['enabled']      = false if Settings.recaptcha['enabled'].nil?
+Settings.recaptcha['public_key'] ||= Settings.recaptcha['public_key']
+Settings.recaptcha['private_key'] ||= Settings.recaptcha['private_key']
+
+
 Settings['shared'] ||= Settingslogic.new({})
 Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
 
diff --git a/config/initializers/recaptcha.rb b/config/initializers/recaptcha.rb
new file mode 100644
index 00000000000..7509e327ae1
--- /dev/null
+++ b/config/initializers/recaptcha.rb
@@ -0,0 +1,6 @@
+if Gitlab.config.recaptcha.enabled
+  Recaptcha.configure do |config|
+    config.public_key  = Gitlab.config.recaptcha['public_key']
+    config.private_key = Gitlab.config.recaptcha['private_key']
+  end
+end
-- 
GitLab