Adds two factor authentication support (#1237).

Patch by Felix Schäfer.


git-svn-id: http://svn.redmine.org/redmine/trunk@19988 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Go MAEDA
2020-08-29 06:21:50 +00:00
parent 657ddfef45
commit 560bca344a
22 changed files with 656 additions and 4 deletions

View File

@@ -68,6 +68,7 @@ require 'redmine/hook'
require 'redmine/hook/listener'
require 'redmine/hook/view_listener'
require 'redmine/plugin'
require 'redmine/twofa'
Redmine::Scm::Base.add "Subversion"
Redmine::Scm::Base.add "Mercurial"

58
lib/redmine/twofa.rb Normal file
View File

@@ -0,0 +1,58 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2020 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine
module Twofa
def self.register_scheme(name, klass)
initialize_schemes
@@schemes[name] = klass
end
def self.available_schemes
schemes.keys
end
def self.for_twofa_scheme(name)
schemes[name]
end
def self.for_user(user)
for_twofa_scheme(user.twofa_scheme).try(:new, user)
end
def self.schemes
initialize_schemes
@@schemes
end
private_class_method :schemes
def self.initialize_schemes
@@schemes ||= { }
scan_builtin_schemes if @@schemes.blank?
end
private_class_method :initialize_schemes
def self.scan_builtin_schemes
Dir[Rails.root.join('lib', 'redmine', 'twofa', '*.rb')].each do |file|
require_dependency file
end
end
private_class_method :scan_builtin_schemes
end
end

127
lib/redmine/twofa/base.rb Normal file
View File

@@ -0,0 +1,127 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2020 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine
module Twofa
class Base
def self.inherited(child)
# require-ing a Base subclass will register it as a 2FA scheme
Redmine::Twofa.register_scheme(scheme_name(child), child)
end
def self.scheme_name(klass = self)
klass.name.demodulize.underscore
end
def scheme_name
self.class.scheme_name
end
def initialize(user)
@user = user
end
def init_pairing!
@user
end
def confirm_pairing!(code)
# make sure an otp is used
if verify_otp!(code)
@user.update!(twofa_scheme: scheme_name)
deliver_twofa_paired
return true
else
return false
end
end
def deliver_twofa_paired
Mailer.security_notification(
@user,
User.current,
{
title: :label_my_account,
message: 'twofa_mail_body_security_notification_paired',
# (mis-)use field here as value wouldn't get localized
field: "twofa__#{scheme_name}__name",
url: { controller: 'my', action: 'account' }
}
).deliver
end
def destroy_pairing!(code)
if verify!(code)
destroy_pairing_without_verify!
return true
else
return false
end
end
def destroy_pairing_without_verify!
@user.update!(twofa_scheme: nil)
deliver_twofa_unpaired
end
def deliver_twofa_unpaired
Mailer.security_notification(
@user,
User.current,
{
title: :label_my_account,
message: 'twofa_mail_body_security_notification_unpaired',
url: { controller: 'my', action: 'account' }
}
).deliver
end
def send_code(controller: nil, action: nil)
# return true only if the scheme sends a code to the user
false
end
def verify!(code)
verify_otp!(code)
end
def verify_otp!(code)
raise 'not implemented'
end
# this will only be used on pairing initialization
def init_pairing_view_variables
otp_confirm_view_variables
end
def otp_confirm_view_variables
{
scheme_name: scheme_name,
resendable: false
}
end
private
def allowed_drift
30
end
end
end
end

68
lib/redmine/twofa/totp.rb Normal file
View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2020 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine
module Twofa
class Totp < Base
def init_pairing!
@user.update!(twofa_totp_key: ROTP::Base32.random)
# reset the cached totp as the key might have changed
@totp = nil
super
end
def destroy_pairing_without_verify!
@user.update!(twofa_totp_key: nil, twofa_totp_last_used_at: nil)
# reset the cached totp as the key might have changed
@totp = nil
super
end
def verify_otp!(code)
# topt codes are white-space-insensitive
code = code.to_s.remove(/[[:space:]]/)
last_verified_at = @user.twofa_totp_last_used_at
verified_at = totp.verify(code.to_s, drift_behind: allowed_drift, after: last_verified_at)
if verified_at
@user.update!(twofa_totp_last_used_at: verified_at)
return true
else
return false
end
end
def provisioning_uri
totp.provisioning_uri(@user.mail)
end
def init_pairing_view_variables
super.merge({
provisioning_uri: provisioning_uri,
totp_key: @user.twofa_totp_key
})
end
private
def totp
@totp ||= ROTP::TOTP.new(@user.twofa_totp_key, issuer: Setting.app_title)
end
end
end
end