rails_base 0.75.6 → 0.80.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/rails_base/rails_base_query_checker.js +36 -0
- data/app/controllers/rails_base/admin_controller.rb +54 -9
- data/app/controllers/rails_base/mfa/evaluation_controller.rb +59 -0
- data/app/controllers/rails_base/mfa/register/sms_controller.rb +45 -0
- data/app/controllers/rails_base/mfa/register/totp_controller.rb +42 -0
- data/app/controllers/rails_base/mfa/validate/sms_controller.rb +83 -0
- data/app/controllers/rails_base/mfa/validate/totp_controller.rb +35 -0
- data/app/controllers/rails_base/secondary_authentication_controller.rb +40 -96
- data/app/controllers/rails_base/user_settings_controller.rb +11 -1
- data/app/controllers/rails_base/users/registrations_controller.rb +1 -1
- data/app/controllers/rails_base/users/sessions_controller.rb +16 -13
- data/app/controllers/rails_base_application_controller.rb +96 -1
- data/app/jobs/twilio_job.rb +1 -1
- data/app/mailers/rails_base/email_verification_mailer.rb +6 -4
- data/app/mailers/rails_base/event_mailer.rb +4 -2
- data/app/mailers/rails_base/mailer_kwarg_inject.rb +31 -0
- data/app/models/rails_base/user_constants.rb +6 -3
- data/app/models/rails_base/user_helper/totp/backup_method_options.rb +33 -0
- data/app/models/rails_base/user_helper/totp/class_options.rb +35 -0
- data/app/models/rails_base/user_helper/totp/consume_method_options.rb +60 -0
- data/app/models/rails_base/user_helper/totp.rb +41 -0
- data/app/models/user.rb +28 -13
- data/app/services/rails_base/authentication/constants.rb +1 -1
- data/app/services/rails_base/authentication/decision_twofa_type.rb +61 -30
- data/app/services/rails_base/authentication/send_forgot_password.rb +0 -1
- data/app/services/rails_base/authentication/single_sign_on_send.rb +1 -1
- data/app/services/rails_base/authentication/sso_verify_email.rb +3 -1
- data/app/services/rails_base/authentication/update_phone_send_verification.rb +2 -2
- data/app/services/rails_base/authentication/verify_forgot_password.rb +8 -11
- data/app/services/rails_base/mfa/decision.rb +70 -0
- data/app/services/rails_base/mfa/encrypt_token.rb +34 -0
- data/app/services/rails_base/mfa/sms/remove.rb +35 -0
- data/app/services/rails_base/{authentication/send_login_mfa_to_user.rb → mfa/sms/send.rb} +19 -13
- data/app/services/rails_base/mfa/sms/validate.rb +105 -0
- data/app/services/rails_base/mfa/strategy/base.rb +44 -0
- data/app/services/rails_base/mfa/strategy/every_request.rb +14 -0
- data/app/services/rails_base/mfa/strategy/skip_every_request.rb +14 -0
- data/app/services/rails_base/mfa/strategy/time_based.rb +24 -0
- data/app/services/rails_base/mfa/totp/helper.rb +21 -0
- data/app/services/rails_base/mfa/totp/otp_metadata.rb +19 -0
- data/app/services/rails_base/mfa/totp/remove.rb +40 -0
- data/app/services/rails_base/mfa/totp/validate_code.rb +52 -0
- data/app/services/rails_base/mfa/totp/validate_temporary_code.rb +37 -0
- data/app/services/rails_base/mfa.rb +18 -0
- data/app/services/rails_base/name_change.rb +3 -3
- data/app/views/layouts/rails_base/application.html.erb +22 -6
- data/app/views/rails_base/devise/passwords/new.html.erb +1 -1
- data/app/views/rails_base/mfa/_switch_mfa_type.html.erb +17 -0
- data/app/views/rails_base/mfa/validate/sms/sms_event_input.html.erb +2 -0
- data/app/views/rails_base/mfa/validate/totp/totp_event_input.html.erb +1 -0
- data/app/views/rails_base/secondary_authentication/reset_password_input.html.erb +4 -0
- data/app/views/rails_base/shared/_enable_mfa_auth_modal.html.erb +1 -1
- data/app/views/rails_base/shared/_logged_in_header.html.erb +1 -25
- data/app/views/rails_base/shared/_modify_mfa_auth_modal.html.erb +102 -3
- data/app/views/rails_base/shared/mfa/sms/_login_input.html.erb +13 -0
- data/app/views/rails_base/shared/mfa/totp/_login_input.html.erb +22 -0
- data/app/views/rails_base/shared/totp/_add_authenticator.html.erb +76 -0
- data/app/views/rails_base/shared/totp/_add_authenticator_modal.html.erb +25 -0
- data/app/views/rails_base/shared/totp/_confirm_code.html.erb +31 -0
- data/app/views/rails_base/shared/totp/_confirm_code_ajax.html.erb +3 -0
- data/app/views/rails_base/shared/totp/_confirm_code_rest.html.erb +5 -0
- data/app/views/rails_base/shared/totp/_remove_authenticator_modal.html.erb +50 -0
- data/app/views/rails_base/user_settings/index.html.erb +84 -1
- data/config/initializers/admin_action_helper.rb +44 -8
- data/config/routes.rb +42 -7
- data/db/migrate/20240808013706_add_totp_to_users.rb +9 -0
- data/db/migrate/20240825012724_reconfigure_mfa_variable_names.rb +10 -0
- data/lib/rails_base/admin/action_helper.rb +0 -1
- data/lib/rails_base/admin/default_index_tile.rb +3 -3
- data/lib/rails_base/config.rb +26 -22
- data/lib/rails_base/configuration/admin.rb +5 -5
- data/lib/rails_base/configuration/base.rb +1 -0
- data/lib/rails_base/configuration/mfa.rb +27 -60
- data/lib/rails_base/configuration/totp.rb +82 -0
- data/lib/rails_base/configuration/twilio.rb +85 -0
- data/lib/rails_base/mfa_event.rb +186 -0
- data/lib/rails_base/version.rb +3 -3
- data/lib/rails_base.rb +1 -0
- data/lib/twilio_helper.rb +3 -3
- metadata +129 -64
- data/app/controllers/rails_base/mfa_auth_controller.rb +0 -50
- data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +0 -32
- data/app/services/rails_base/authentication/mfa_validator.rb +0 -88
- data/app/views/rails_base/mfa_auth/mfa_code.html.erb +0 -11
- data/app/views/rails_base/secondary_authentication/forgot_password.html.erb +0 -9
@@ -8,30 +8,54 @@ module RailsBase::Authentication
|
|
8
8
|
# default return values
|
9
9
|
context.set_mfa_randomized_token = false
|
10
10
|
context.sign_in_user = false
|
11
|
+
unless user.email_validated
|
12
|
+
email_context = validate_email_context!
|
13
|
+
check_success!(result: email_context)
|
14
|
+
log(level: :info, msg: "User #{user.id}: redirect_url: #{context.redirect_url}, sign_in_user: #{context.sign_in_user}, flash: #{context.flash}")
|
15
|
+
log_exit
|
16
|
+
return
|
17
|
+
end
|
11
18
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
sign_in_user_context!
|
20
|
-
context.flash = { notice: "Welcome. You have succesfully signed in. We suggest enabling 2fa authentication to secure your account" }
|
21
|
-
nil
|
22
|
-
end
|
23
|
-
else
|
24
|
-
validate_email_context!
|
25
|
-
end
|
19
|
+
unless RailsBase.config.mfa.enable?
|
20
|
+
log(level: :info, msg: "MFA on app is not enabled. Bypassing")
|
21
|
+
sign_in_user_context!
|
22
|
+
context.flash = { notice: "Welcome. You have succesfully signed in." }
|
23
|
+
log_exit
|
24
|
+
return
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
mfa_decision = RailsBase::Mfa::Decision.(user: user)
|
28
|
+
check_success!(result: mfa_decision)
|
29
|
+
mfa_type_result = nil
|
30
|
+
case mfa_decision.mfa_type
|
31
|
+
when RailsBase::Mfa::SMS
|
32
|
+
mfa_type_result = sms_enabled_context!(decision: mfa_decision)
|
33
|
+
when RailsBase::Mfa::OTP
|
34
|
+
totp_enabled_context!(decision: mfa_decision)
|
35
|
+
when RailsBase::Mfa::NONE
|
36
|
+
# no MFA type enabled on account
|
37
|
+
sign_in_user_context!
|
38
|
+
context.flash = { notice: "Welcome. You have succesfully signed in." }
|
39
|
+
context.session = { add_mfa_button: true }
|
40
|
+
else
|
41
|
+
raise "Unknown MFA type provided"
|
30
42
|
end
|
43
|
+
check_success!(result: mfa_type_result)
|
44
|
+
log_exit
|
45
|
+
end
|
31
46
|
|
47
|
+
def log_exit
|
32
48
|
log(level: :info, msg: "User #{user.id}: redirect_url: #{context.redirect_url}, sign_in_user: #{context.sign_in_user}, flash: #{context.flash}")
|
33
49
|
end
|
34
50
|
|
51
|
+
def check_success!(result:)
|
52
|
+
return if result.nil?
|
53
|
+
return if result.success?
|
54
|
+
|
55
|
+
log(level: :error, msg: "Service error bubbled up. Failing with: #{result.message}")
|
56
|
+
context.fail!(message: result.message)
|
57
|
+
end
|
58
|
+
|
35
59
|
def validate_email_context!
|
36
60
|
# user has signed up but have not validated their email
|
37
61
|
context.redirect_url = Constants::URL_HELPER.auth_static_path
|
@@ -48,23 +72,30 @@ module RailsBase::Authentication
|
|
48
72
|
context.sign_in_user = true
|
49
73
|
end
|
50
74
|
|
51
|
-
def
|
52
|
-
if
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
context.
|
57
|
-
|
58
|
-
|
59
|
-
context.flash = { notice: "
|
60
|
-
|
75
|
+
def totp_enabled_context!(decision:)
|
76
|
+
if decision.mfa_require
|
77
|
+
log(level: :warn, msg: "TOTP MFA required for user")
|
78
|
+
context.redirect_url = RailsBase.url_routes.mfa_with_event_path(mfa_event: :login)
|
79
|
+
context.flash = { notice: "Additional Verification requested" }
|
80
|
+
context.token_ttl = 2.minutes.from_now
|
81
|
+
else
|
82
|
+
sign_in_user_context!
|
83
|
+
context.flash = { notice: "Welcome. You have succesfully signed in via #{decision.mfa_type.to_s.upcase} MFA." }
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def sms_enabled_context!(decision:)
|
89
|
+
if decision.mfa_require
|
90
|
+
log(level: :warn, msg: "SMS MFA required for user")
|
91
|
+
context.redirect_url = RailsBase.url_routes.mfa_with_event_path(mfa_event: :login)
|
92
|
+
context.flash = { notice: "Please check your mobile device. We sent an SMS for MFA verification" }
|
93
|
+
result = RailsBase::Mfa::Sms::Send.call(user: user)
|
61
94
|
context.token_ttl = result.short_lived_data.death_time if result.success?
|
62
95
|
result
|
63
96
|
else
|
64
97
|
sign_in_user_context!
|
65
|
-
|
66
|
-
context.flash = { notice: "Welcome. You have succesfully signed in. You will be mfa free for another #{mfa_free_words}" }
|
67
|
-
log(level: :info, msg: "User is mfa free for another #{mfa_free_words}")
|
98
|
+
context.flash = { notice: "Welcome. You have succesfully signed in via #{decision.mfa_type.to_s.upcase} MFA." }
|
68
99
|
nil
|
69
100
|
end
|
70
101
|
end
|
@@ -4,7 +4,6 @@ module RailsBase::Authentication
|
|
4
4
|
|
5
5
|
def call
|
6
6
|
user = User.find_for_authentication(email: email)
|
7
|
-
|
8
7
|
if user.nil?
|
9
8
|
log(level: :warn, msg: "Failed to find email assocaited to #{email}. Not sending email")
|
10
9
|
context.fail!(message: "Failed to send forget password to #{email}", redirect_url: '')
|
@@ -32,7 +32,7 @@ module RailsBase::Authentication
|
|
32
32
|
token_type: token_type,
|
33
33
|
url_redirect: url_redirect
|
34
34
|
}
|
35
|
-
datum = SingleSignOnCreate.
|
35
|
+
datum = SingleSignOnCreate.(**params)
|
36
36
|
context.fail!(message: 'Failed to create SSO token. Try again') if datum.failure?
|
37
37
|
|
38
38
|
url = sso_url(data: datum.data.data)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module RailsBase::Authentication
|
2
4
|
class SsoVerifyEmail < RailsBase::ServiceBase
|
3
5
|
delegate :verification, to: :context
|
@@ -15,7 +17,7 @@ module RailsBase::Authentication
|
|
15
17
|
user: user,
|
16
18
|
purpose: Constants::SSOVE_PURPOSE
|
17
19
|
}
|
18
|
-
context.encrypted_val =
|
20
|
+
context.encrypted_val = RailsBase::Mfa::EncryptToken.(params).encrypted_val
|
19
21
|
end
|
20
22
|
|
21
23
|
def validate_datum?(datum)
|
@@ -14,7 +14,7 @@ module RailsBase::Authentication
|
|
14
14
|
# requires a bit of a restructure that I dont have time for
|
15
15
|
update_user_number!
|
16
16
|
|
17
|
-
twilio_sms =
|
17
|
+
twilio_sms = RailsBase::Mfa::Sms::Send.call(user: user.reload)
|
18
18
|
|
19
19
|
if twilio_sms.failure?
|
20
20
|
log(level: :error, msg: "Failed with #{twilio_sms.message}")
|
@@ -22,7 +22,7 @@ module RailsBase::Authentication
|
|
22
22
|
end
|
23
23
|
context.expires_at = twilio_sms.short_lived_data.death_time
|
24
24
|
context.mfa_randomized_token =
|
25
|
-
|
25
|
+
RailsBase::Mfa::EncryptToken.(user: user, expires_at: context.expires_at, purpose: Constants::MSET_PURPOSE).encrypted_val
|
26
26
|
end
|
27
27
|
|
28
28
|
def update_user_number!
|
@@ -3,22 +3,19 @@ module RailsBase::Authentication
|
|
3
3
|
delegate :data, to: :context
|
4
4
|
|
5
5
|
def call
|
6
|
-
mfa_flow = false
|
7
6
|
data_point = short_lived_data
|
8
7
|
validate_datum?(data_point)
|
9
8
|
|
10
9
|
log(level: :info, msg: "Validated user 2fa email #{data_point[:user].full_name}")
|
10
|
+
|
11
11
|
context.user = data_point[:user]
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
log(level: :warn, msg: "Attempted to send MFA to user from #{self.class.name}: Exiting with #{result.message}")
|
19
|
-
context.fail!(message: result.message, redirect_url: Constants::URL_HELPER.new_user_password_path, level: :warn)
|
12
|
+
mfa_decision = RailsBase::Mfa::Decision.(force_mfa: true, user: data_point[:user])
|
13
|
+
|
14
|
+
if context.mfa_flow = mfa_decision.mfa_require
|
15
|
+
log(level: :info, msg: "User has #{mfa_decision.mfa_options} mfa options enabled. MFA is required to reset password")
|
16
|
+
else
|
17
|
+
log(level: :info, msg: "User has no MFA options enabled. MFA is NOT required to reset password")
|
20
18
|
end
|
21
|
-
context.mfa_flow = true
|
22
19
|
end
|
23
20
|
|
24
21
|
def validate_datum?(datum)
|
@@ -32,7 +29,7 @@ module RailsBase::Authentication
|
|
32
29
|
|
33
30
|
log(level: :warn, msg: "Could not find MFA code. Incorrect MFA code. User is doing something fishy.")
|
34
31
|
|
35
|
-
context.fail!(message: Constants::MV_FISHY, redirect_url: Constants::URL_HELPER.
|
32
|
+
context.fail!(message: Constants::MV_FISHY, redirect_url: Constants::URL_HELPER.unauthenticated_root_path, level: :warn)
|
36
33
|
end
|
37
34
|
|
38
35
|
def short_lived_data
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa
|
4
|
+
class Decision < RailsBase::ServiceBase
|
5
|
+
delegate :user, to: :context
|
6
|
+
|
7
|
+
def call
|
8
|
+
unless RailsBase.config.mfa.enable?
|
9
|
+
execute_nil("Application")
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
if user.mfa_otp_enabled
|
14
|
+
execute_otp
|
15
|
+
elsif user.mfa_sms_enabled
|
16
|
+
execute_sms
|
17
|
+
else
|
18
|
+
execute_nil("User")
|
19
|
+
end
|
20
|
+
|
21
|
+
available_mfa_options!
|
22
|
+
end
|
23
|
+
|
24
|
+
def available_mfa_options!
|
25
|
+
mfa_options = []
|
26
|
+
mfa_options << OTP if user.mfa_otp_enabled
|
27
|
+
mfa_options << SMS if user.mfa_sms_enabled
|
28
|
+
|
29
|
+
context.mfa_options = mfa_options
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute_otp
|
33
|
+
log(level: :info, msg: "MFA type OTP is enabled on user. Executing OTP workflow")
|
34
|
+
result = reauth_strategy_class.(user: user, force: force_mfa, mfa_type: OTP, mfa_last_used: user.last_mfa_otp_login)
|
35
|
+
require_mfa = result.request_mfa
|
36
|
+
|
37
|
+
context_clues(type: OTP, require_mfa: require_mfa)
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute_sms
|
41
|
+
log(level: :info, msg: "MFA type SMS is enabled on user. Executing OTP workflow")
|
42
|
+
result = reauth_strategy_class.(user: user, force: force_mfa, mfa_type: SMS, mfa_last_used: user.last_mfa_sms_login)
|
43
|
+
require_mfa = result.request_mfa
|
44
|
+
|
45
|
+
context_clues(type: SMS, require_mfa: require_mfa)
|
46
|
+
end
|
47
|
+
|
48
|
+
def execute_nil(classify)
|
49
|
+
log(level: :info, msg: "#{classify} does not have any MFA type enabled. Skipping")
|
50
|
+
context_clues(type: NONE, require_mfa: false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def context_clues(type:, require_mfa:)
|
54
|
+
context.mfa_type = type
|
55
|
+
context.mfa_require = require_mfa
|
56
|
+
end
|
57
|
+
|
58
|
+
def force_mfa
|
59
|
+
context.force_mfa.nil? ? false : context.force_mfa
|
60
|
+
end
|
61
|
+
|
62
|
+
def reauth_strategy_class
|
63
|
+
RailsBase.config.mfa.reauth_strategy
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate!
|
67
|
+
raise "Expected user to be a User. Received #{user.class}" unless user.is_a? User
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa
|
4
|
+
class EncryptToken < RailsBase::ServiceBase
|
5
|
+
delegate :user, to: :context
|
6
|
+
delegate :expires_at, to: :context
|
7
|
+
delegate :purpose, to: :context
|
8
|
+
|
9
|
+
def call
|
10
|
+
params = {
|
11
|
+
value: value,
|
12
|
+
purpose: purpose || RailsBase::Authentication::Constants::MSET_PURPOSE,
|
13
|
+
expires_at: expires_at
|
14
|
+
}
|
15
|
+
|
16
|
+
context.encrypted_val = RailsBase::Encryption.encode(**params)
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
# user_id with the same expires_at will return the same Encryption token
|
21
|
+
# to overcome this, do 2 things
|
22
|
+
# 1: Rotate the secret on every boot (ensures tplem changes on semi regular basis)
|
23
|
+
# 2: Add rand strings to the hash -- Ensures the token is different every time
|
24
|
+
{ user_id: user.id, rand: rand.to_s, expires_at: expires_at }.to_json
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate!
|
28
|
+
raise "Expected user to be a User. Received #{user.class}" unless user.is_a? User
|
29
|
+
|
30
|
+
time_class = ActiveSupport::TimeWithZone
|
31
|
+
raise "Expected expires_at to be a Received #{time_class}. Received #{expires_at.class}" unless expires_at.is_a? time_class
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Sms
|
4
|
+
class Remove < RailsBase::ServiceBase
|
5
|
+
delegate :password, to: :context
|
6
|
+
delegate :session_mfa_user_id, to: :context
|
7
|
+
delegate :current_user, to: :context
|
8
|
+
delegate :sms_code, to: :context
|
9
|
+
delegate :mfa_event, to: :context
|
10
|
+
|
11
|
+
def call
|
12
|
+
password_result = RailsBase::Authentication::AuthenticateUser.(email: current_user.email, current_user: current_user, password: password)
|
13
|
+
if password_result.failure?
|
14
|
+
log(level: :debug, msg: "Password validation failed. Unable to continue")
|
15
|
+
context.fail!(message: password_result.message)
|
16
|
+
end
|
17
|
+
|
18
|
+
validate_code = Validate.(mfa_event:,sms_code:, session_mfa_user_id:, current_user:)
|
19
|
+
|
20
|
+
if validate_code.failure?
|
21
|
+
log(level: :warn, msg: "Unable to confirm SMS OTP code. Will not remove")
|
22
|
+
context.fail!(message: "Incorrect One Time Password Code")
|
23
|
+
end
|
24
|
+
|
25
|
+
current_user.update!(mfa_sms_enabled: false, last_mfa_sms_login: nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate!
|
29
|
+
raise 'Expected the current_user passed' if current_user.nil?
|
30
|
+
raise 'Expected the sms_code passed' if sms_code.nil?
|
31
|
+
raise 'session_mfa_user_id is not present' if session_mfa_user_id.nil?
|
32
|
+
raise 'password is not present' if password.nil?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'velocity_limiter'
|
4
|
+
require 'twilio_helper'
|
3
5
|
|
4
|
-
module RailsBase::
|
5
|
-
class
|
6
|
+
module RailsBase::Mfa::Sms
|
7
|
+
class Send < RailsBase::ServiceBase
|
6
8
|
include ActionView::Helpers::DateHelper
|
7
9
|
include VelocityLimiter
|
8
10
|
|
@@ -24,14 +26,18 @@ module RailsBase::Authentication
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def send_twilio!(code)
|
27
|
-
TwilioJob.perform_later(message: message(code), to:
|
28
|
-
log(level: :info, msg: "Sent twilio message to #{
|
29
|
+
TwilioJob.perform_later(message: message(code), to: phone_number)
|
30
|
+
log(level: :info, msg: "Sent twilio message to #{phone_number}")
|
29
31
|
rescue StandardError => e
|
30
32
|
log(level: :error, msg: "Error caught #{e.class.name}")
|
31
|
-
log(level: :error, msg: "Failed to send sms to #{
|
33
|
+
log(level: :error, msg: "Failed to send sms to #{phone_number}")
|
32
34
|
context.fail!(message: "Failed to send sms. Please retry logging in.")
|
33
35
|
end
|
34
36
|
|
37
|
+
def phone_number
|
38
|
+
context.phone_number || user.phone_number
|
39
|
+
end
|
40
|
+
|
35
41
|
def message(code)
|
36
42
|
"Hello #{user.full_name}. Here is your verification code #{code}."
|
37
43
|
end
|
@@ -40,25 +46,25 @@ module RailsBase::Authentication
|
|
40
46
|
params = {
|
41
47
|
user: user,
|
42
48
|
max_use: MAX_USE_COUNT,
|
43
|
-
reason: Constants::MFA_REASON,
|
49
|
+
reason: RailsBase::Authentication::Constants::MFA_REASON,
|
44
50
|
data_use: DATA_USE,
|
45
|
-
ttl: Constants::SLMTU_TTL,
|
51
|
+
ttl: RailsBase::Authentication::Constants::SLMTU_TTL,
|
46
52
|
expires_at: expires_at,
|
47
|
-
length: Constants::MFA_LENGTH,
|
53
|
+
length: RailsBase::Authentication::Constants::MFA_LENGTH,
|
48
54
|
}
|
49
55
|
ShortLivedData.create_data_key(**params)
|
50
56
|
end
|
51
57
|
|
52
58
|
def velocity_max_in_frame
|
53
|
-
RailsBase.config.
|
59
|
+
RailsBase.config.twilio.twilio_velocity_max_in_frame
|
54
60
|
end
|
55
61
|
|
56
62
|
def velocity_max
|
57
|
-
RailsBase.config.
|
63
|
+
RailsBase.config.twilio.twilio_velocity_max
|
58
64
|
end
|
59
65
|
|
60
66
|
def velocity_frame
|
61
|
-
RailsBase.config.
|
67
|
+
RailsBase.config.twilio.twilio_velocity_frame
|
62
68
|
end
|
63
69
|
|
64
70
|
def cache_key
|
@@ -71,7 +77,7 @@ module RailsBase::Authentication
|
|
71
77
|
raise "Expected expires_at to be a ActiveSupport::TimeWithZone. Given #{expires_at.class}"
|
72
78
|
end
|
73
79
|
|
74
|
-
raise NoPhoneNumber, "No phone for user [#{user.id}] [#{
|
80
|
+
raise NoPhoneNumber, "No phone for user [#{user.id}] [#{phone_number}]" if phone_number.nil?
|
75
81
|
end
|
76
82
|
end
|
77
83
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Sms
|
4
|
+
class Validate < RailsBase::ServiceBase
|
5
|
+
delegate :params, to: :context
|
6
|
+
delegate :session_mfa_user_id, to: :context
|
7
|
+
delegate :current_user, to: :context
|
8
|
+
delegate :input_reason, to: :context
|
9
|
+
delegate :sms_code, to: :context
|
10
|
+
delegate :mfa_event, to: :context
|
11
|
+
|
12
|
+
def call
|
13
|
+
if sms_code.present?
|
14
|
+
log(level: :info, msg: "Raw sms code was passed in")
|
15
|
+
mfa_code = sms_code
|
16
|
+
else
|
17
|
+
log(level: :info, msg: "Code Array was passed in. Manipulating Data first")
|
18
|
+
array = convert_to_array
|
19
|
+
if array.length != RailsBase::Authentication::Constants::MFA_LENGTH
|
20
|
+
log(level: :warn, msg: "Not enough params for MFA code. Given #{array}. Expected of length #{RailsBase::Authentication::Constants::MFA_LENGTH}")
|
21
|
+
context.fail!(message: RailsBase::Authentication::Constants::MV_FISHY, redirect_url: RailsBase.url_routes.new_user_session_path, level: :alert)
|
22
|
+
end
|
23
|
+
|
24
|
+
mfa_code = array.join
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
log(level: :info, msg: "mfa code received: #{mfa_code}")
|
29
|
+
datum = get_short_lived_datum(mfa_code)
|
30
|
+
log(level: :info, msg: "Datum returned with: #{datum}")
|
31
|
+
|
32
|
+
validate_datum?(datum)
|
33
|
+
validate_user_consistency?(datum)
|
34
|
+
validate_current_user?(datum) if current_user
|
35
|
+
|
36
|
+
context.user = datum[:user]
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_current_user?(datum)
|
40
|
+
return true if current_user.id == datum[:user].id
|
41
|
+
|
42
|
+
# User MFA for a different user matched the session token
|
43
|
+
# However, those did not match the current user signed in
|
44
|
+
# Something is very 🐟
|
45
|
+
log(level: :error, msg: "Someone is a teapot. Current logged in user does not equal mfa code.")
|
46
|
+
context.fail!(message: 'You are a teapot', redirect_url: RailsBase.url_routes.signout_path, level: :warn)
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_datum?(datum)
|
50
|
+
return true if datum[:valid]
|
51
|
+
|
52
|
+
if datum[:found]
|
53
|
+
# MFA is either expired or the incorrect reason. Either way it does not match
|
54
|
+
msg = "Errors with MFA: #{datum[:invalid_reason].join(", ")}. Please login again"
|
55
|
+
log(level: :warn, msg: msg)
|
56
|
+
context.fail!(message: msg, redirect_url: RailsBase.url_routes.new_user_session_path, level: :warn)
|
57
|
+
end
|
58
|
+
|
59
|
+
# MFA does not exist for any reason type
|
60
|
+
log(level: :warn, msg: "Could not find MFA code. Incorrect MFA code")
|
61
|
+
|
62
|
+
context.fail!(message: "Incorrect SMS code.", redirect_url: RailsBase.url_routes.mfa_with_event_path(mfa_event: mfa_event.event, type: RailsBase::Mfa::SMS), level: :warn)
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_user_consistency?(datum)
|
66
|
+
return true if datum[:user].id == session_mfa_user_id.to_i
|
67
|
+
log(level: :warn, msg: "Datum user does not match session user. [#{datum[:user].id}, #{session_mfa_user_id.to_i}]")
|
68
|
+
|
69
|
+
# MFA session token user does not match the datum user
|
70
|
+
# Something is very 🐟
|
71
|
+
context.fail!(message: RailsBase::Authentication::Constants::MV_FISHY, redirect_url: RailsBase.url_routes.new_user_session_path, level: :alert)
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_short_lived_datum(mfa_code)
|
75
|
+
log(level: :debug, msg: "Looking for #{mfa_code} with reason #{reason}")
|
76
|
+
ShortLivedData.find_datum(data: mfa_code, reason: reason)
|
77
|
+
end
|
78
|
+
|
79
|
+
def convert_to_array
|
80
|
+
array = []
|
81
|
+
return array unless params.dig(:mfa).respond_to? :keys
|
82
|
+
|
83
|
+
RailsBase::Authentication::Constants::MFA_LENGTH.times do |index|
|
84
|
+
var_name = "#{RailsBase::Authentication::Constants::MV_BASE_NAME}#{index}".to_sym
|
85
|
+
array << params[:mfa][var_name]
|
86
|
+
end
|
87
|
+
|
88
|
+
array.compact
|
89
|
+
end
|
90
|
+
|
91
|
+
def reason
|
92
|
+
input_reason || RailsBase::Authentication::Constants::MFA_REASON
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate!
|
96
|
+
if sms_code.nil?
|
97
|
+
raise 'params is not present' if params.nil?
|
98
|
+
end
|
99
|
+
|
100
|
+
raise 'mfa_event is expected to be a RailsBase::MfaEvent' unless RailsBase::MfaEvent === mfa_event
|
101
|
+
|
102
|
+
raise 'session_mfa_user_id is not present' if session_mfa_user_id.nil?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Strategy
|
4
|
+
class Base < RailsBase::ServiceBase
|
5
|
+
delegate :user, to: :context
|
6
|
+
delegate :force, to: :context
|
7
|
+
delegate :mfa_type, to: :context
|
8
|
+
delegate :mfa_last_used, to: :context
|
9
|
+
|
10
|
+
def call
|
11
|
+
log(level: :info, msg: "#{user_prepend} : MFA strategy against #{mfa_type}")
|
12
|
+
|
13
|
+
if require_mfa?(user: user, mfa_type: mfa_type, mfa_last_used: mfa_last_used)
|
14
|
+
mfa_required
|
15
|
+
else
|
16
|
+
if force
|
17
|
+
log(level: :info, msg: "#{user_prepend} : MFA strategy was not required at this time. However -- Force option was passed in")
|
18
|
+
mfa_required
|
19
|
+
else
|
20
|
+
mfa_not_required
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def mfa_required
|
26
|
+
log(level: :info, msg: "#{user_prepend} : MFA strategy is required at this time based on the strategy")
|
27
|
+
context.request_mfa = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def mfa_not_required
|
31
|
+
log(level: :info, msg: "#{user_prepend} : MFA strategy is NOT required at this time")
|
32
|
+
context.request_mfa = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def user_prepend
|
36
|
+
"[#{user.full_name} (#{user.id})]"
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate!
|
40
|
+
raise "Expected user to be a User. Received #{user.class}" unless User === user
|
41
|
+
raise "Expected mfa_type to be a present" if mfa_type.nil?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Strategy
|
4
|
+
class EveryRequest < Base
|
5
|
+
def self.description
|
6
|
+
"MFA is always requried"
|
7
|
+
end
|
8
|
+
|
9
|
+
def require_mfa?(...)
|
10
|
+
log(level: :info, msg: "#{user_prepend} : Strategy dictates user must re-auth via MFA")
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Strategy
|
4
|
+
class SkipEveryRequest < Base
|
5
|
+
def self.description
|
6
|
+
"MFA is never requried"
|
7
|
+
end
|
8
|
+
|
9
|
+
def require_mfa?(...)
|
10
|
+
log(level: :info, msg: "#{user_prepend} : Strategy dictates user will never re-auth via MFA")
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Strategy
|
4
|
+
class TimeBased < Base
|
5
|
+
def self.description
|
6
|
+
"MFA is required every #{RailsBase.config.mfa.reauth_duration}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def require_mfa?(mfa_last_used:, **)
|
10
|
+
if mfa_last_used.nil?
|
11
|
+
log(level: :info, msg: "#{user_prepend} : User has not succesfully logged into mfa")
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
log(level: :info, msg: "#{user_prepend} : User last used mfa #{mfa_last_used.utc} (vs #{Time.now.utc})")
|
16
|
+
required_line = mfa_last_used.utc + RailsBase.config.mfa.reauth_duration
|
17
|
+
log(level: :info, msg: "#{user_prepend} : User required to reauth after #{required_line}")
|
18
|
+
status = required_line < Time.now.utc
|
19
|
+
log(level: :info, msg: "#{user_prepend} : User required to reauth? #{status}")
|
20
|
+
|
21
|
+
status
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Totp
|
4
|
+
module Helper
|
5
|
+
def secret
|
6
|
+
context.otp_secret || user.reload.otp_secret
|
7
|
+
end
|
8
|
+
|
9
|
+
def otp
|
10
|
+
@otp ||= ROTP::TOTP.new(secret)
|
11
|
+
end
|
12
|
+
|
13
|
+
def current_code
|
14
|
+
otp.at(Time.now)
|
15
|
+
end
|
16
|
+
|
17
|
+
def lgp
|
18
|
+
@lgp ||= "[#{user.full_name}:(#{user.id})] :"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsBase::Mfa::Totp
|
4
|
+
class OtpMetadata < RailsBase::ServiceBase
|
5
|
+
delegate :user, to: :context
|
6
|
+
|
7
|
+
def call
|
8
|
+
context.metadata = user.otp_metadata(safe: true)
|
9
|
+
rescue => e
|
10
|
+
log(level: :error, msg: "Failed to retreive OTP data: #{e.message}")
|
11
|
+
log(level: :error, msg: e.backtrace)
|
12
|
+
context.fail!(message: "Failed to retrieve Metadata for Code")
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
raise "Expected user to be a User. " unless User === user
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|