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.
Files changed (86) 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/mfa/sms/_login_input.html.erb +13 -0
  57. data/app/views/rails_base/shared/mfa/totp/_login_input.html.erb +22 -0
  58. data/app/views/rails_base/shared/totp/_add_authenticator.html.erb +76 -0
  59. data/app/views/rails_base/shared/totp/_add_authenticator_modal.html.erb +25 -0
  60. data/app/views/rails_base/shared/totp/_confirm_code.html.erb +31 -0
  61. data/app/views/rails_base/shared/totp/_confirm_code_ajax.html.erb +3 -0
  62. data/app/views/rails_base/shared/totp/_confirm_code_rest.html.erb +5 -0
  63. data/app/views/rails_base/shared/totp/_remove_authenticator_modal.html.erb +50 -0
  64. data/app/views/rails_base/user_settings/index.html.erb +84 -1
  65. data/config/initializers/admin_action_helper.rb +44 -8
  66. data/config/routes.rb +42 -7
  67. data/db/migrate/20240808013706_add_totp_to_users.rb +9 -0
  68. data/db/migrate/20240825012724_reconfigure_mfa_variable_names.rb +10 -0
  69. data/lib/rails_base/admin/action_helper.rb +0 -1
  70. data/lib/rails_base/admin/default_index_tile.rb +3 -3
  71. data/lib/rails_base/config.rb +26 -22
  72. data/lib/rails_base/configuration/admin.rb +5 -5
  73. data/lib/rails_base/configuration/base.rb +1 -0
  74. data/lib/rails_base/configuration/mfa.rb +27 -60
  75. data/lib/rails_base/configuration/totp.rb +82 -0
  76. data/lib/rails_base/configuration/twilio.rb +85 -0
  77. data/lib/rails_base/mfa_event.rb +186 -0
  78. data/lib/rails_base/version.rb +3 -3
  79. data/lib/rails_base.rb +1 -0
  80. data/lib/twilio_helper.rb +3 -3
  81. metadata +129 -64
  82. data/app/controllers/rails_base/mfa_auth_controller.rb +0 -50
  83. data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +0 -32
  84. data/app/services/rails_base/authentication/mfa_validator.rb +0 -88
  85. data/app/views/rails_base/mfa_auth/mfa_code.html.erb +0 -11
  86. 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