Commit b1b5b221 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'feature/force-tfa' into 'master'

Require two factor authentication

Allow Admins to force 2FA on their users, as discussed here: gitlab-org/gitlab-ce#3783

See merge request !2155
parents 350d6550 1249289f
...@@ -17,6 +17,7 @@ v 8.3.1 ...@@ -17,6 +17,7 @@ v 8.3.1
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu) - Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Fix LDAP identity and user retrieval when special characters are used - Fix LDAP identity and user retrieval when special characters are used
- Move Sidekiq-cron configuration to gitlab.yml - Move Sidekiq-cron configuration to gitlab.yml
- Enable forcing Two-Factor authentication sitewide, with optional grace period
v 8.3.0 v 8.3.0
- Bump rack-attack to 4.3.1 for security fix (Stan Hu) - Bump rack-attack to 4.3.1 for security fix (Stan Hu)
......
...@@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_branch_protection, :default_branch_protection,
:signup_enabled, :signup_enabled,
:signin_enabled, :signin_enabled,
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled, :gravatar_enabled,
:twitter_sharing_enabled, :twitter_sharing_enabled,
:sign_in_text, :sign_in_text,
......
...@@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base ...@@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :reject_blocked! before_action :reject_blocked!
before_action :check_password_expiration before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check before_action :ldap_security_check
before_action :default_headers before_action :default_headers
before_action :add_gon_variables before_action :add_gon_variables
...@@ -223,6 +224,12 @@ class ApplicationController < ActionController::Base ...@@ -223,6 +224,12 @@ class ApplicationController < ActionController::Base
end end
end end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path
end
end
def ldap_security_check def ldap_security_check
if current_user && current_user.requires_ldap_check? if current_user && current_user.requires_ldap_check?
unless Gitlab::LDAP::Access.allowed?(current_user) unless Gitlab::LDAP::Access.allowed?(current_user)
...@@ -357,6 +364,23 @@ class ApplicationController < ActionController::Base ...@@ -357,6 +364,23 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git') current_application_settings.import_sources.include?('git')
end end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
def two_factor_grace_period
current_application_settings.two_factor_grace_period
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def skip_two_factor?
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
def redirect_to_home_page_url? def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page # If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections # Don't redirect to the default URL to prevent endless redirections
......
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
def new def new
unless current_user.otp_secret unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32) current_user.otp_secret = User.generate_otp_secret(32)
current_user.save! end
unless current_user.otp_grace_period_started_at && two_factor_grace_period
current_user.otp_grace_period_started_at = Time.current
end
current_user.save! if current_user.changed?
if two_factor_grace_period_expired?
flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
end end
@qr_code = build_qr_code @qr_code = build_qr_code
...@@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
redirect_to profile_account_path redirect_to profile_account_path
end end
def skip
if two_factor_grace_period_expired?
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
else
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path
end
end
private private
def build_qr_code def build_qr_code
......
...@@ -50,5 +50,17 @@ module AuthHelper ...@@ -50,5 +50,17 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s) current_user.identities.exists?(provider: provider.to_s)
end end
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
def two_factor_grace_period_expired?
current_user.otp_grace_period_started_at &&
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
extend self extend self
end end
...@@ -2,32 +2,34 @@ ...@@ -2,32 +2,34 @@
# #
# Table name: application_settings # Table name: application_settings
# #
# id :integer not null, primary key # id :integer not null, primary key
# default_projects_limit :integer # default_projects_limit :integer
# signup_enabled :boolean # signup_enabled :boolean
# signin_enabled :boolean # signin_enabled :boolean
# gravatar_enabled :boolean # gravatar_enabled :boolean
# sign_in_text :text # sign_in_text :text
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# home_page_url :string(255) # home_page_url :string(255)
# default_branch_protection :integer default(2) # default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE) # twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text # restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE) # version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null # max_attachment_size :integer default(10), not null
# default_project_visibility :integer # default_project_visibility :integer
# default_snippet_visibility :integer # default_snippet_visibility :integer
# restricted_signup_domains :text # restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE) # user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255) # after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null # session_expire_delay :integer default(10080), not null
# import_sources :text # import_sources :text
# help_page_text :text # help_page_text :text
# admin_notification_email :string(255) # admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null # shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null # max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255) # runners_registration_token :string(255)
# require_two_factor_authentication :boolean default(TRUE)
# two_factor_grace_period :integer default(48)
# #
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
...@@ -58,6 +60,9 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -58,6 +60,9 @@ class ApplicationSetting < ActiveRecord::Base
allow_blank: true, allow_blank: true,
email: true email: true
validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil? unless value.nil?
value.each do |level| value.each do |level|
...@@ -112,6 +117,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -112,6 +117,8 @@ class ApplicationSetting < ActiveRecord::Base
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48
) )
end end
......
...@@ -104,6 +104,18 @@ ...@@ -104,6 +104,18 @@
= f.label :signin_enabled do = f.label :signin_enabled do
= f.check_box :signin_enabled = f.check_box :signin_enabled
Sign-in enabled Sign-in enabled
.form-group
= f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
Require all users to setup Two-Factor authentication
.form-group
= f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group .form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2' = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
...@@ -38,3 +38,4 @@ ...@@ -38,3 +38,4 @@
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
.form-actions .form-actions
= submit_tag 'Submit', class: 'btn btn-success' = submit_tag 'Submit', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
...@@ -297,6 +297,7 @@ Rails.application.routes.draw do ...@@ -297,6 +297,7 @@ Rails.application.routes.draw do
resource :two_factor_auth, only: [:new, :create, :destroy] do resource :two_factor_auth, only: [:new, :create, :destroy] do
member do member do
post :codes post :codes
patch :skip
end end
end end
end end
......
class AddTfaToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
t.boolean :require_two_factor_authentication, default: false
t.integer :two_factor_grace_period, default: 48
end
end
end
class AddTfaAdditionalFields < ActiveRecord::Migration
def change
change_table :users do |t|
t.datetime :otp_grace_period_started_at, null: true
end
end
end
...@@ -50,6 +50,8 @@ ActiveRecord::Schema.define(version: 20151224123230) do ...@@ -50,6 +50,8 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.boolean "shared_runners_enabled", default: true, null: false t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token" t.string "runners_registration_token"
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
...@@ -838,6 +840,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do ...@@ -838,6 +840,7 @@ ActiveRecord::Schema.define(version: 20151224123230) do
t.integer "layout", default: 0 t.integer "layout", default: 0
t.boolean "hide_project_limit", default: false t.boolean "hide_project_limit", default: false
t.string "unlock_token" t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
end end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
...@@ -6,3 +6,4 @@ ...@@ -6,3 +6,4 @@
- [Information exclusivity](information_exclusivity.md) - [Information exclusivity](information_exclusivity.md)
- [Reset your root password](reset_root_password.md) - [Reset your root password](reset_root_password.md)
- [User File Uploads](user_file_uploads.md) - [User File Uploads](user_file_uploads.md)
- [Enforce Two-Factor authentication](two_factor_authentication.md)
# Enforce Two-factor Authentication (2FA)
Two-factor Authentication (2FA) provides an additional level of security to your
users' GitLab account. Once enabled, in addition to supplying their username and
password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
## Enabling 2FA
Users on GitLab, can enable it without any admin's intervention. If you want to
enforce everyone to setup 2FA, you can choose from two different ways:
1. Enforce on next login
2. Suggest on next login, but allow a grace period before enforcing.
In the Admin area under **Settings** (`/admin/application_settings`), look for
the "Sign-in Restrictions" area, where you can configure both.
If you want 2FA enforcement to take effect on next login, change the grace
period to `0`
## Disabling 2FA for everyone
There may be some special situations where you want to disable 2FA for everyone
even when forced 2FA is disabled. There is a rake task for that:
```
# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
# if you've installed GitLab from source
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.**
...@@ -98,4 +98,56 @@ feature 'Login', feature: true do ...@@ -98,4 +98,56 @@ feature 'Login', feature: true do
expect(page).to have_content('Invalid login or password.') expect(page).to have_content('Invalid login or password.')
end end
end end
describe 'with required two-factor authentication enabled' do
let(:user) { create(:user) }
before(:each) { stub_application_setting(require_two_factor_authentication: true) }
context 'with grace period defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 48)
login_with(user)
end
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account until')
end
it 'two-factor configuration is skippable' do
expect(current_path).to eq new_profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
end
end
context 'after the grace period' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
end
it 'two-factor configuration is not skippable' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end
end
context 'without grace pariod defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 0)
login_with(user)
end
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
end
end
end
end end
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
# admin_notification_email :string(255) # admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null # shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null # max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
# #
require 'spec_helper' require 'spec_helper'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment