rails_base 0.75.5 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/_standardized_collapse.html.erb +28 -0
- 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/appearance.rb +0 -2
- 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
|