summaryrefslogtreecommitdiff
path: root/app
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
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')
-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
-rw-r--r--app/models/user.rb15
-rw-r--r--app/views/devise/sessions/two_factor.html.haml10
-rw-r--r--app/views/layouts/nav/_profile.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml31
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml11
-rw-r--r--app/views/profiles/two_factor_auths/codes.html.haml5
-rw-r--r--app/views/profiles/two_factor_auths/create.html.haml6
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml23
12 files changed, 222 insertions, 8 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
diff --git a/app/models/user.rb b/app/models/user.rb
index a70cbaa518b..d088d2d8630 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -50,6 +50,11 @@
# bitbucket_access_token :string(255)
# bitbucket_access_token_secret :string(255)
# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean
+# otp_backup_codes :text
# public_email :string(255) default(""), not null
#
@@ -70,8 +75,14 @@ class User < ActiveRecord::Base
default_value_for :hide_no_password, false
default_value_for :theme_id, gitlab_config.default_theme
- devise :database_authenticatable, :lockable, :async,
- :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable
+ devise :two_factor_authenticatable,
+ otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp
+
+ devise :two_factor_backupable, otp_number_of_backup_codes: 10
+ serialize :otp_backup_codes, JSON
+
+ devise :lockable, :async, :recoverable, :rememberable, :trackable,
+ :validatable, :omniauthable, :confirmable, :registerable
attr_accessor :force_random_password
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
new file mode 100644
index 00000000000..22b2c1a186b
--- /dev/null
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -0,0 +1,10 @@
+%div
+ .login-box
+ .login-heading
+ %h3 Two-factor Authentication
+ .login-body
+ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
+ = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor authentication code', required: true, autofocus: true
+ %p.help-block.hint If you've lost your phone, you may enter one of your recovery codes.
+ .prepend-top-20
+ = f.submit "Verify code", class: "btn btn-save"
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 31d8ed3ed86..ac37fd4c1c1 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -4,7 +4,7 @@
= icon('user fw')
%span
Profile
- = nav_link(controller: :accounts) do
+ = nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account', data: {placement: 'right'} do
= icon('gear fw')
%span
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 1c3a3d68aca..6ac60b01f85 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -26,6 +26,33 @@
%span You don`t have one yet. Click generate to fix it.
= f.submit 'Generate', class: "btn success btn-build-token"
+ - unless current_user.ldap_user?
+ %fieldset
+ - if current_user.otp_required_for_login
+ %legend.text-success
+ = icon('check')
+ Two-factor Authentication enabled
+ %div
+ .pull-right
+ = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
+ data: { confirm: 'Are you sure?' }
+ %p
+ If you lose your recovery codes you can
+ %strong
+ = succeed ',' do
+ = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' }
+ invalidating all previous codes.
+
+ - else
+ %legend Two-factor Authentication
+ %div
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ %p
+ Each time you log in you’ll be required to provide your username and
+ password as usual, plus a randomly-generated code from your phone.
+ %div
+ = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
- if show_profile_social_tab?
%fieldset
@@ -38,7 +65,7 @@
class: "btn btn-lg #{'active' if oauth_active?(provider)}"
- if oauth_active?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
- %i.fa.fa-close
+ = icon('close')
- if show_profile_username_tab?
%fieldset.update-username
@@ -52,7 +79,7 @@
&nbsp;
.loading-gif.hide
%p
- %i.fa.fa-spinner.fa-spin
+ = icon('spinner spin')
Saving new username
%p.light
= user_url(@user)
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
new file mode 100644
index 00000000000..1b1395eaa17
--- /dev/null
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -0,0 +1,11 @@
+%p.slead
+ Should you ever lose your phone, each of these recovery codes can be used one
+ time each to regain access to your account. Please save them in a safe place.
+
+.codes.well
+ %ul
+ - @codes.each do |code|
+ %li
+ %span.monospace= code
+
+= link_to 'Proceed', profile_account_path, class: 'btn btn-success'
diff --git a/app/views/profiles/two_factor_auths/codes.html.haml b/app/views/profiles/two_factor_auths/codes.html.haml
new file mode 100644
index 00000000000..addf356697a
--- /dev/null
+++ b/app/views/profiles/two_factor_auths/codes.html.haml
@@ -0,0 +1,5 @@
+- page_title 'Recovery Codes', 'Two-factor Authentication'
+
+%h3.page-title Two-factor Authentication Recovery codes
+%hr
+= render 'codes'
diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml
new file mode 100644
index 00000000000..e330aadac13
--- /dev/null
+++ b/app/views/profiles/two_factor_auths/create.html.haml
@@ -0,0 +1,6 @@
+- page_title 'Two-factor Authentication', 'Account'
+
+.alert.alert-success
+ Congratulations! You have enabled Two-factor Authentication!
+
+= render 'codes'
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
new file mode 100644
index 00000000000..fe03a259a12
--- /dev/null
+++ b/app/views/profiles/two_factor_auths/new.html.haml
@@ -0,0 +1,23 @@
+- page_title 'Two-factor Authentication', 'Account'
+
+%h2.page-title Two-Factor Authentication (2FA)
+%p
+ Download the Google Authenticator application from App Store for iOS or
+ Google Play for Android and scan this code.
+
+%hr
+
+= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal' do |f|
+ - if @error
+ .alert.alert-danger
+ = @error
+ .form-group
+ .col-sm-2
+ .col-sm-10
+ = raw @qr_code
+ .form-group
+ = label_tag :pin_code, nil, class: "control-label"
+ .col-sm-10
+ = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
+ .form-actions
+ = submit_tag 'Submit', class: 'btn btn-success'