mirror of
https://github.com/redmine/redmine.git
synced 2025-12-15 21:10:27 +01:00
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:
@@ -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
58
lib/redmine/twofa.rb
Normal 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
127
lib/redmine/twofa/base.rb
Normal 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
68
lib/redmine/twofa/totp.rb
Normal 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
|
||||
Reference in New Issue
Block a user