summaryrefslogtreecommitdiff
path: root/app/controllers
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-05-12 09:41:27 +0000
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-05-12 09:41:27 +0000
commit4a373be8617814f74fa1bfa99740daecc4fe8278 (patch)
treea84b923215c43efa5a82eed984e00c4e7d318493 /app/controllers
parent8e4dcbb8fb4823a464dfdd8b62075df124ca5bc6 (diff)
parent22badc13136369e202dc6df06a62456110879ee4 (diff)
downloadgitlab-ce-4a373be8617814f74fa1bfa99740daecc4fe8278.tar.gz
Merge branch '2fa' into 'master'
Two-factor authentication Implement's Two-factor authentication using tokens. - [X] Authentication logic - [X] Enable/disable 2FA feature - [x] Make 2-step login process if 2FA enabled - [x] Backup codes - [x] Backup code removed after being used - [x] Check backup codes for mysql db (mention mysql limitation if applied) - [x] Add tests - [x] Test if https://github.com/tinfoil/devise-two-factor#disabling-automatic-login-after-password-resets applies, and address if so - [x] Wait for fixed version of `attr_encrypted` or fork and use forked version - https://github.com/attr-encrypted/attr_encrypted/issues/155 Fixes http://feedback.gitlab.com/forums/176466-general/suggestions/4516817-implement-two-factor-authentication-2fa See merge request !474
Diffstat (limited to 'app/controllers')
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/passwords_controller.rb21
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb49
-rw-r--r--app/controllers/sessions_controller.rb55
4 files changed, 124 insertions, 3 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index eee10d6c22a..8ce881c7414 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -252,7 +252,7 @@ class ApplicationController < ActionController::Base
end
def configure_permitted_parameters
- devise_parameter_sanitizer.sanitize(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me) }
+ devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) }
end
def hexdigest(string)
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index dcbbe5baa4b..88459d4080a 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -15,4 +15,25 @@ class PasswordsController < Devise::PasswordsController
respond_with(resource)
end
end
+
+ # After a user resets their password, prompt for 2FA code if enabled instead
+ # of signing in automatically
+ #
+ # See http://git.io/vURrI
+ def update
+ super do |resource|
+ # TODO (rspeicher): In Devise master (> 3.4.1), we can set
+ # `Devise.sign_in_after_reset_password = false` and avoid this mess.
+ if resource.errors.empty? && resource.try(:otp_required_for_login?)
+ resource.unlock_access! if unlockable?(resource)
+
+ # Since we are not signing this user in, we use the :updated_not_active
+ # message which only contains "Your password was changed successfully."
+ set_flash_message(:notice, :updated_not_active) if is_flashing_format?
+
+ # Redirect to sign in so they can enter 2FA code
+ respond_with(resource, location: new_session_path(resource)) and return
+ end
+ end
+ end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
new file mode 100644
index 00000000000..30ee6891733
--- /dev/null
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -0,0 +1,49 @@
+class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
+ def new
+ unless current_user.otp_secret
+ current_user.otp_secret = User.generate_otp_secret
+ current_user.save!
+ end
+
+ @qr_code = build_qr_code
+ end
+
+ def create
+ if current_user.valid_otp?(params[:pin_code])
+ current_user.otp_required_for_login = true
+ @codes = current_user.generate_otp_backup_codes!
+ current_user.save!
+
+ render 'create'
+ else
+ @error = 'Invalid pin code'
+ @qr_code = build_qr_code
+ render 'new'
+ end
+ end
+
+ def codes
+ @codes = current_user.generate_otp_backup_codes!
+ current_user.save!
+ end
+
+ def destroy
+ current_user.update_attributes({
+ otp_required_for_login: false,
+ encrypted_otp_secret: nil,
+ encrypted_otp_secret_iv: nil,
+ encrypted_otp_secret_salt: nil,
+ otp_backup_codes: nil
+ })
+
+ redirect_to profile_account_path
+ end
+
+ private
+
+ def build_qr_code
+ issuer = "GitLab | #{current_user.email}"
+ uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
+ RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 3f11d7afe6f..d4ff0d97561 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,4 +1,12 @@
class SessionsController < Devise::SessionsController
+ prepend_before_action :authenticate_with_two_factor, only: [:create]
+
+ # This action comes from DeviseController, but because we call `sign_in`
+ # manually inside `authenticate_with_two_factor`, not skipping this action
+ # would cause a "You are already signed in." error message to be shown upon
+ # successful login.
+ skip_before_action :require_no_authentication, only: [:create]
+
def new
redirect_path =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
@@ -14,7 +22,7 @@ class SessionsController < Devise::SessionsController
# Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully.
- unless redirect_path == '/users/sign_in'
+ unless redirect_path == new_user_session_path
store_location_for(:redirect, redirect_path)
end
@@ -27,11 +35,54 @@ class SessionsController < Devise::SessionsController
def create
super do |resource|
- # User has successfully signed in, so clear any unused reset tokens
+ # User has successfully signed in, so clear any unused reset token
if resource.reset_password_token.present?
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
end
end
+
+ private
+
+ def user_params
+ params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
+ end
+
+ def find_user
+ if user_params[:login]
+ User.by_login(user_params[:login])
+ elsif user_params[:otp_attempt] && session[:otp_user_id]
+ User.find(session[:otp_user_id])
+ end
+ end
+
+ def authenticate_with_two_factor
+ user = self.resource = find_user
+
+ return unless user && user.otp_required_for_login
+
+ if user_params[:otp_attempt].present? && session[:otp_user_id]
+ if valid_otp_attempt?(user)
+ # Remove any lingering user data from login
+ session.delete(:otp_user_id)
+
+ sign_in(user) and return
+ else
+ flash.now[:alert] = 'Invalid two-factor code.'
+ render :two_factor and return
+ end
+ else
+ if user && user.valid_password?(user_params[:password])
+ # Save the user's ID to session so we can ask for a one-time password
+ session[:otp_user_id] = user.id
+ render :two_factor and return
+ end
+ end
+ end
+
+ def valid_otp_attempt?(user)
+ user.valid_otp?(user_params[:otp_attempt]) ||
+ user.invalidate_otp_backup_code!(user_params[:otp_attempt])
+ end
end