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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/rails_base/rails_base_query_checker.js +36 -0
  3. data/app/controllers/rails_base/admin_controller.rb +54 -9
  4. data/app/controllers/rails_base/mfa/evaluation_controller.rb +59 -0
  5. data/app/controllers/rails_base/mfa/register/sms_controller.rb +45 -0
  6. data/app/controllers/rails_base/mfa/register/totp_controller.rb +42 -0
  7. data/app/controllers/rails_base/mfa/validate/sms_controller.rb +83 -0
  8. data/app/controllers/rails_base/mfa/validate/totp_controller.rb +35 -0
  9. data/app/controllers/rails_base/secondary_authentication_controller.rb +40 -96
  10. data/app/controllers/rails_base/user_settings_controller.rb +11 -1
  11. data/app/controllers/rails_base/users/registrations_controller.rb +1 -1
  12. data/app/controllers/rails_base/users/sessions_controller.rb +16 -13
  13. data/app/controllers/rails_base_application_controller.rb +96 -1
  14. data/app/jobs/twilio_job.rb +1 -1
  15. data/app/mailers/rails_base/email_verification_mailer.rb +6 -4
  16. data/app/mailers/rails_base/event_mailer.rb +4 -2
  17. data/app/mailers/rails_base/mailer_kwarg_inject.rb +31 -0
  18. data/app/models/rails_base/user_constants.rb +6 -3
  19. data/app/models/rails_base/user_helper/totp/backup_method_options.rb +33 -0
  20. data/app/models/rails_base/user_helper/totp/class_options.rb +35 -0
  21. data/app/models/rails_base/user_helper/totp/consume_method_options.rb +60 -0
  22. data/app/models/rails_base/user_helper/totp.rb +41 -0
  23. data/app/models/user.rb +28 -13
  24. data/app/services/rails_base/authentication/constants.rb +1 -1
  25. data/app/services/rails_base/authentication/decision_twofa_type.rb +61 -30
  26. data/app/services/rails_base/authentication/send_forgot_password.rb +0 -1
  27. data/app/services/rails_base/authentication/single_sign_on_send.rb +1 -1
  28. data/app/services/rails_base/authentication/sso_verify_email.rb +3 -1
  29. data/app/services/rails_base/authentication/update_phone_send_verification.rb +2 -2
  30. data/app/services/rails_base/authentication/verify_forgot_password.rb +8 -11
  31. data/app/services/rails_base/mfa/decision.rb +70 -0
  32. data/app/services/rails_base/mfa/encrypt_token.rb +34 -0
  33. data/app/services/rails_base/mfa/sms/remove.rb +35 -0
  34. data/app/services/rails_base/{authentication/send_login_mfa_to_user.rb → mfa/sms/send.rb} +19 -13
  35. data/app/services/rails_base/mfa/sms/validate.rb +105 -0
  36. data/app/services/rails_base/mfa/strategy/base.rb +44 -0
  37. data/app/services/rails_base/mfa/strategy/every_request.rb +14 -0
  38. data/app/services/rails_base/mfa/strategy/skip_every_request.rb +14 -0
  39. data/app/services/rails_base/mfa/strategy/time_based.rb +24 -0
  40. data/app/services/rails_base/mfa/totp/helper.rb +21 -0
  41. data/app/services/rails_base/mfa/totp/otp_metadata.rb +19 -0
  42. data/app/services/rails_base/mfa/totp/remove.rb +40 -0
  43. data/app/services/rails_base/mfa/totp/validate_code.rb +52 -0
  44. data/app/services/rails_base/mfa/totp/validate_temporary_code.rb +37 -0
  45. data/app/services/rails_base/mfa.rb +18 -0
  46. data/app/services/rails_base/name_change.rb +3 -3
  47. data/app/views/layouts/rails_base/application.html.erb +22 -6
  48. data/app/views/rails_base/devise/passwords/new.html.erb +1 -1
  49. data/app/views/rails_base/mfa/_switch_mfa_type.html.erb +17 -0
  50. data/app/views/rails_base/mfa/validate/sms/sms_event_input.html.erb +2 -0
  51. data/app/views/rails_base/mfa/validate/totp/totp_event_input.html.erb +1 -0
  52. data/app/views/rails_base/secondary_authentication/reset_password_input.html.erb +4 -0
  53. data/app/views/rails_base/shared/_enable_mfa_auth_modal.html.erb +1 -1
  54. data/app/views/rails_base/shared/_logged_in_header.html.erb +1 -25
  55. data/app/views/rails_base/shared/_modify_mfa_auth_modal.html.erb +102 -3
  56. data/app/views/rails_base/shared/_standardized_collapse.html.erb +28 -0
  57. data/app/views/rails_base/shared/mfa/sms/_login_input.html.erb +13 -0
  58. data/app/views/rails_base/shared/mfa/totp/_login_input.html.erb +22 -0
  59. data/app/views/rails_base/shared/totp/_add_authenticator.html.erb +76 -0
  60. data/app/views/rails_base/shared/totp/_add_authenticator_modal.html.erb +25 -0
  61. data/app/views/rails_base/shared/totp/_confirm_code.html.erb +31 -0
  62. data/app/views/rails_base/shared/totp/_confirm_code_ajax.html.erb +3 -0
  63. data/app/views/rails_base/shared/totp/_confirm_code_rest.html.erb +5 -0
  64. data/app/views/rails_base/shared/totp/_remove_authenticator_modal.html.erb +50 -0
  65. data/app/views/rails_base/user_settings/index.html.erb +84 -1
  66. data/config/initializers/admin_action_helper.rb +44 -8
  67. data/config/routes.rb +42 -7
  68. data/db/migrate/20240808013706_add_totp_to_users.rb +9 -0
  69. data/db/migrate/20240825012724_reconfigure_mfa_variable_names.rb +10 -0
  70. data/lib/rails_base/admin/action_helper.rb +0 -1
  71. data/lib/rails_base/admin/default_index_tile.rb +3 -3
  72. data/lib/rails_base/config.rb +26 -22
  73. data/lib/rails_base/configuration/admin.rb +5 -5
  74. data/lib/rails_base/configuration/appearance.rb +0 -2
  75. data/lib/rails_base/configuration/base.rb +1 -0
  76. data/lib/rails_base/configuration/mfa.rb +27 -60
  77. data/lib/rails_base/configuration/totp.rb +82 -0
  78. data/lib/rails_base/configuration/twilio.rb +85 -0
  79. data/lib/rails_base/mfa_event.rb +186 -0
  80. data/lib/rails_base/version.rb +3 -3
  81. data/lib/rails_base.rb +1 -0
  82. data/lib/twilio_helper.rb +3 -3
  83. metadata +129 -64
  84. data/app/controllers/rails_base/mfa_auth_controller.rb +0 -50
  85. data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +0 -32
  86. data/app/services/rails_base/authentication/mfa_validator.rb +0 -88
  87. data/app/views/rails_base/mfa_auth/mfa_code.html.erb +0 -11
  88. 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
- mfa_decision =
13
- if user.email_validated
14
- if RailsBase.config.mfa.enable? && user.mfa_enabled
15
- mfa_enabled_context!
16
- else
17
- # user has signed up and validated email
18
- # user does not have mfa enabled
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
- if mfa_decision && mfa_decision.failure?
28
- log(level: :error, msg: "Service error bubbled up. Failing with: #{mfa_decision.message}")
29
- context.fail!(message: mfa_decision.message)
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 mfa_enabled_context!
52
- if user.past_mfa_time_duration?
53
- # user has signed up and validated email
54
- # user has mfa enabled
55
- log(level: :warn, msg: "User needs to go through mfa flow. #{user.last_mfa_login} < #{User.time_bound}")
56
- context.redirect_url = Constants::URL_HELPER.mfa_code_path
57
- context.set_mfa_randomized_token = true
58
- context.mfa_purpose = nil # use default
59
- context.flash = { notice: "Please check your mobile device. We sent an SMS for 2fa verification" }
60
- result = SendLoginMfaToUser.call(user: user)
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
- mfa_free_words = distance_of_time_in_words(user.last_mfa_login, User.time_bound)
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.call(**params)
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 = MfaSetEncryptToken.call(params).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 = SendLoginMfaToUser.call(user: user.reload)
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
- MfaSetEncryptToken.call(user: user, expires_at: context.expires_at, purpose: Constants::MSET_PURPOSE).encrypted_val
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
- context.encrypted_val =
13
- MfaSetEncryptToken.call(user: data_point[:user], expires_at: Time.zone.now + 10.minutes, purpose: Constants::VFP_PURPOSE).encrypted_val
14
- return unless data_point[:user].mfa_enabled
15
-
16
- result = SendLoginMfaToUser.call(user: data_point[:user], expires_at: Time.zone.now + 10.minutes)
17
- if result.failure?
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.authenticated_root_path, level: :warn)
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
- require 'twilio_helper'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'velocity_limiter'
4
+ require 'twilio_helper'
3
5
 
4
- module RailsBase::Authentication
5
- class SendLoginMfaToUser < RailsBase::ServiceBase
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: user.phone_number)
28
- log(level: :info, msg: "Sent twilio message to #{user.phone_number}")
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 #{user.phone_number}")
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.mfa.twilio_velocity_max_in_frame
59
+ RailsBase.config.twilio.twilio_velocity_max_in_frame
54
60
  end
55
61
 
56
62
  def velocity_max
57
- RailsBase.config.mfa.twilio_velocity_max
63
+ RailsBase.config.twilio.twilio_velocity_max
58
64
  end
59
65
 
60
66
  def velocity_frame
61
- RailsBase.config.mfa.twilio_velocity_frame
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}] [#{user.phone_number}]" if user.phone_number.nil?
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