rails_base 0.51.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 (194) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +32 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/rails_base/manifest.js +3 -0
  6. data/app/assets/images/rails_base/favicon.ico +0 -0
  7. data/app/assets/javascripts/rails_base/admin.js +2 -0
  8. data/app/assets/javascripts/rails_base/application.js +22 -0
  9. data/app/assets/javascripts/rails_base/cable.js +13 -0
  10. data/app/assets/javascripts/rails_base/mfa_auth.coffee +3 -0
  11. data/app/assets/javascripts/rails_base/secondary_authentication.coffee +3 -0
  12. data/app/assets/javascripts/rails_base/sessions.js +152 -0
  13. data/app/assets/javascripts/rails_base/user_settings.coffee +3 -0
  14. data/app/assets/stylesheets/rails_base/admin.css +4 -0
  15. data/app/assets/stylesheets/rails_base/application.scss +15 -0
  16. data/app/assets/stylesheets/rails_base/mfa_auth.scss +3 -0
  17. data/app/assets/stylesheets/rails_base/scaffolds.scss +84 -0
  18. data/app/assets/stylesheets/rails_base/secondary_authentication.scss +3 -0
  19. data/app/assets/stylesheets/rails_base/user_settings.scss +3 -0
  20. data/app/controllers/rails_base/admin_controller.rb +315 -0
  21. data/app/controllers/rails_base/application_controller.rb +153 -0
  22. data/app/controllers/rails_base/errors_controller.rb +29 -0
  23. data/app/controllers/rails_base/mfa_auth_controller.rb +50 -0
  24. data/app/controllers/rails_base/secondary_authentication_controller.rb +224 -0
  25. data/app/controllers/rails_base/switch_user_controller.rb +29 -0
  26. data/app/controllers/rails_base/user_settings_controller.rb +81 -0
  27. data/app/controllers/rails_base/users/passwords_controller.rb +19 -0
  28. data/app/controllers/rails_base/users/registrations_controller.rb +80 -0
  29. data/app/controllers/rails_base/users/sessions_controller.rb +108 -0
  30. data/app/helpers/rails_base/admin_helper.rb +107 -0
  31. data/app/helpers/rails_base/appearance_helper.rb +58 -0
  32. data/app/helpers/rails_base/application_helper.rb +26 -0
  33. data/app/helpers/rails_base/capture_reference_helper.rb +57 -0
  34. data/app/helpers/rails_base/mfa_auth_helper.rb +2 -0
  35. data/app/helpers/rails_base/secondary_authentication_helper.rb +2 -0
  36. data/app/helpers/rails_base/user_field_validators.rb +108 -0
  37. data/app/helpers/rails_base/user_settings_helper.rb +22 -0
  38. data/app/jobs/rails_base/application_job.rb +10 -0
  39. data/app/jobs/twilio_job.rb +9 -0
  40. data/app/mailers/rails_base/application_mailer.rb +9 -0
  41. data/app/mailers/rails_base/email_verification_mailer.rb +22 -0
  42. data/app/mailers/rails_base/event_mailer.rb +16 -0
  43. data/app/models/admin_action.rb +119 -0
  44. data/app/models/rails_base/application_record.rb +22 -0
  45. data/app/models/rails_base/user_constants.rb +28 -0
  46. data/app/models/secret.rb +37 -0
  47. data/app/models/short_lived_data.rb +132 -0
  48. data/app/models/user.rb +143 -0
  49. data/app/services/rails_base/admin_risky_mfa_send.rb +80 -0
  50. data/app/services/rails_base/admin_update_attribute.rb +100 -0
  51. data/app/services/rails_base/authentication/authenticate_user.rb +28 -0
  52. data/app/services/rails_base/authentication/constants.rb +60 -0
  53. data/app/services/rails_base/authentication/decision_twofa_type.rb +76 -0
  54. data/app/services/rails_base/authentication/destroy_user.rb +45 -0
  55. data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +32 -0
  56. data/app/services/rails_base/authentication/mfa_validator.rb +88 -0
  57. data/app/services/rails_base/authentication/modify_password.rb +67 -0
  58. data/app/services/rails_base/authentication/send_forgot_password.rb +26 -0
  59. data/app/services/rails_base/authentication/send_login_mfa_to_user.rb +77 -0
  60. data/app/services/rails_base/authentication/send_verification_email.rb +103 -0
  61. data/app/services/rails_base/authentication/session_token_verifier.rb +31 -0
  62. data/app/services/rails_base/authentication/single_sign_on_create.rb +44 -0
  63. data/app/services/rails_base/authentication/single_sign_on_send.rb +101 -0
  64. data/app/services/rails_base/authentication/single_sign_on_verify.rb +42 -0
  65. data/app/services/rails_base/authentication/sso_verify_email.rb +43 -0
  66. data/app/services/rails_base/authentication/update_phone_send_verification.rb +46 -0
  67. data/app/services/rails_base/authentication/verify_forgot_password.rb +46 -0
  68. data/app/services/rails_base/email_change.rb +20 -0
  69. data/app/services/rails_base/encryption.rb +87 -0
  70. data/app/services/rails_base/name_change.rb +71 -0
  71. data/app/services/rails_base/service_base.rb +65 -0
  72. data/app/services/rails_base/service_logging.rb +23 -0
  73. data/app/views/layouts/rails_base/application.html.erb +185 -0
  74. data/app/views/layouts/rails_base/mailer.html.erb +13 -0
  75. data/app/views/layouts/rails_base/mailer.text.erb +1 -0
  76. data/app/views/new.html.erb +4 -0
  77. data/app/views/rails_base/admin/history.html.erb +26 -0
  78. data/app/views/rails_base/admin/index.html.erb +149 -0
  79. data/app/views/rails_base/admin/show_config.html.erb +18 -0
  80. data/app/views/rails_base/devise/confirmations/new.html.erb +16 -0
  81. data/app/views/rails_base/devise/mailer/confirmation_instructions.html.erb +5 -0
  82. data/app/views/rails_base/devise/mailer/email_changed.html.erb +7 -0
  83. data/app/views/rails_base/devise/mailer/password_change.html.erb +3 -0
  84. data/app/views/rails_base/devise/mailer/reset_password_instructions.html.erb +8 -0
  85. data/app/views/rails_base/devise/mailer/unlock_instructions.html.erb +7 -0
  86. data/app/views/rails_base/devise/passwords/edit.html.erb +25 -0
  87. data/app/views/rails_base/devise/passwords/new.html.erb +27 -0
  88. data/app/views/rails_base/devise/registrations/edit.html.erb +43 -0
  89. data/app/views/rails_base/devise/registrations/new.html.erb +123 -0
  90. data/app/views/rails_base/devise/sessions/new.html.erb +4 -0
  91. data/app/views/rails_base/devise/shared/_error_messages.html.erb +15 -0
  92. data/app/views/rails_base/devise/shared/_links.html.erb +25 -0
  93. data/app/views/rails_base/devise/unlocks/new.html.erb +16 -0
  94. data/app/views/rails_base/email_verification_mailer/email_verification.html.erb +25 -0
  95. data/app/views/rails_base/email_verification_mailer/event.html.erb +20 -0
  96. data/app/views/rails_base/email_verification_mailer/forgot_password.html.erb +22 -0
  97. data/app/views/rails_base/errors/internal_error.html.erb +1 -0
  98. data/app/views/rails_base/errors/not_found.html.erb +1 -0
  99. data/app/views/rails_base/errors/unacceptable.html.erb +1 -0
  100. data/app/views/rails_base/event_mailer/event.html.erb +10 -0
  101. data/app/views/rails_base/mfa_auth/mfa_code.html.erb +10 -0
  102. data/app/views/rails_base/secondary_authentication/after_email_login_session_new.html.erb +3 -0
  103. data/app/views/rails_base/secondary_authentication/forgot_password.html.erb +9 -0
  104. data/app/views/rails_base/secondary_authentication/remove_me.html.erb +1 -0
  105. data/app/views/rails_base/secondary_authentication/static.html.erb +5 -0
  106. data/app/views/rails_base/shared/_admin_actions_modal.html.erb +65 -0
  107. data/app/views/rails_base/shared/_admin_config_class.html.erb +52 -0
  108. data/app/views/rails_base/shared/_admin_history.html.erb +86 -0
  109. data/app/views/rails_base/shared/_admin_modify_email.html.erb +78 -0
  110. data/app/views/rails_base/shared/_admin_modify_name.html.erb +107 -0
  111. data/app/views/rails_base/shared/_admin_modify_phone.html.erb +87 -0
  112. data/app/views/rails_base/shared/_admin_modify_text.html.erb +35 -0
  113. data/app/views/rails_base/shared/_admin_risky_change.html.erb +57 -0
  114. data/app/views/rails_base/shared/_admin_risky_mfa.html.erb +74 -0
  115. data/app/views/rails_base/shared/_admin_selector_dropdown.html.erb +70 -0
  116. data/app/views/rails_base/shared/_admin_toggle_button.html.erb +72 -0
  117. data/app/views/rails_base/shared/_admin_warning_alert.html.erb +7 -0
  118. data/app/views/rails_base/shared/_appearance_mode_selector.html.erb +183 -0
  119. data/app/views/rails_base/shared/_custom_form_validation_javascript.html.erb +129 -0
  120. data/app/views/rails_base/shared/_enable_mfa_auth_modal.html.erb +105 -0
  121. data/app/views/rails_base/shared/_error_pages.html.erb +123 -0
  122. data/app/views/rails_base/shared/_logged_in_header.html.erb +123 -0
  123. data/app/views/rails_base/shared/_logged_out_header.html.erb +14 -0
  124. data/app/views/rails_base/shared/_mfa_input_layout.html.erb +5 -0
  125. data/app/views/rails_base/shared/_mfa_input_layout_default.html.erb +97 -0
  126. data/app/views/rails_base/shared/_mfa_input_layout_fallback.html.erb +55 -0
  127. data/app/views/rails_base/shared/_modify_mfa_auth_modal.html.erb +20 -0
  128. data/app/views/rails_base/shared/_password_confirm_javascript.html.erb +71 -0
  129. data/app/views/rails_base/shared/_reset_password_form.html.erb +111 -0
  130. data/app/views/rails_base/shared/_session_create_form.html.erb +32 -0
  131. data/app/views/rails_base/shared/_session_timeout_modal.html.erb +76 -0
  132. data/app/views/rails_base/switch_user/_widget.html.erb +5 -0
  133. data/app/views/rails_base/user_settings/_confirm_destroy_user.html.erb +42 -0
  134. data/app/views/rails_base/user_settings/_destroy_user.html.erb +106 -0
  135. data/app/views/rails_base/user_settings/_modify_name.html.erb +71 -0
  136. data/app/views/rails_base/user_settings/_modify_password.html.erb +101 -0
  137. data/app/views/rails_base/user_settings/_modify_password_update_password.html.erb +2 -0
  138. data/app/views/rails_base/user_settings/index.html.erb +54 -0
  139. data/config/initializers/01_rails_config.rb +19 -0
  140. data/config/initializers/admin_action_helper.rb +88 -0
  141. data/config/initializers/browser.rb +4 -0
  142. data/config/initializers/default_logged_in_headers.rb +23 -0
  143. data/config/initializers/devise.rb +314 -0
  144. data/config/initializers/encryption.rb +2 -0
  145. data/config/initializers/switch_user.rb +58 -0
  146. data/config/initializers/switch_user_helper.rb +29 -0
  147. data/config/locales/devise.en.yml +65 -0
  148. data/config/locales/en.yml +58 -0
  149. data/config/routes.rb +114 -0
  150. data/db/migrate/20210212175453_devise_create_rails_base_users.rb +56 -0
  151. data/db/migrate/20210212190537_create_rails_base_short_lived_data.rb +19 -0
  152. data/db/migrate/20210212192645_create_rails_base_secrets.rb +11 -0
  153. data/db/migrate/20210406015744_create_rails_base_admin_actions.rb +17 -0
  154. data/db/seeds.rb +23 -0
  155. data/lib/link_decision_helper.rb +71 -0
  156. data/lib/rails_base.rb +50 -0
  157. data/lib/rails_base/admin/action_cache.rb +99 -0
  158. data/lib/rails_base/admin/action_helper.rb +134 -0
  159. data/lib/rails_base/admin/default_index_tile.rb +176 -0
  160. data/lib/rails_base/admin/index_tile.rb +186 -0
  161. data/lib/rails_base/config.rb +52 -0
  162. data/lib/rails_base/configuration/active_job.rb +38 -0
  163. data/lib/rails_base/configuration/admin.rb +231 -0
  164. data/lib/rails_base/configuration/app.rb +52 -0
  165. data/lib/rails_base/configuration/appearance.rb +131 -0
  166. data/lib/rails_base/configuration/authentication.rb +37 -0
  167. data/lib/rails_base/configuration/base.rb +209 -0
  168. data/lib/rails_base/configuration/display/background_color.rb +25 -0
  169. data/lib/rails_base/configuration/display/btn_danger.rb +25 -0
  170. data/lib/rails_base/configuration/display/btn_dark.rb +25 -0
  171. data/lib/rails_base/configuration/display/btn_info.rb +25 -0
  172. data/lib/rails_base/configuration/display/btn_light.rb +25 -0
  173. data/lib/rails_base/configuration/display/btn_primary.rb +25 -0
  174. data/lib/rails_base/configuration/display/btn_secondary.rb +25 -0
  175. data/lib/rails_base/configuration/display/btn_success.rb +25 -0
  176. data/lib/rails_base/configuration/display/btn_warning.rb +25 -0
  177. data/lib/rails_base/configuration/display/footer.rb +54 -0
  178. data/lib/rails_base/configuration/display/navbar.rb +25 -0
  179. data/lib/rails_base/configuration/display/table_body.rb +25 -0
  180. data/lib/rails_base/configuration/display/table_header.rb +25 -0
  181. data/lib/rails_base/configuration/display/text.rb +26 -0
  182. data/lib/rails_base/configuration/exceptions_app.rb +25 -0
  183. data/lib/rails_base/configuration/login_behavior.rb +17 -0
  184. data/lib/rails_base/configuration/mailer.rb +116 -0
  185. data/lib/rails_base/configuration/mfa.rb +84 -0
  186. data/lib/rails_base/configuration/owner.rb +17 -0
  187. data/lib/rails_base/configuration/redis.rb +29 -0
  188. data/lib/rails_base/configuration/user.rb +43 -0
  189. data/lib/rails_base/engine.rb +51 -0
  190. data/lib/rails_base/version.rb +10 -0
  191. data/lib/tasks/rails_base_tasks.rake +4 -0
  192. data/lib/twilio_helper.rb +26 -0
  193. data/lib/velocity_limiter.rb +91 -0
  194. metadata +619 -0
@@ -0,0 +1,28 @@
1
+ module RailsBase::Authentication
2
+ class AuthenticateUser < RailsBase::ServiceBase
3
+ delegate :email, to: :context
4
+ delegate :password, to: :context
5
+ delegate :current_user, to: :context
6
+
7
+ def call
8
+ user = current_user || User.find_for_authentication(email: email)
9
+ valid = user.present? && user.valid_password?(password)
10
+ if valid
11
+ log(level: :info, msg: "Correctly found valid user_id #{user.id}")
12
+ context.user = user
13
+ else
14
+ log(level: :warn, msg: "Failed to validate credentials")
15
+ log(level: :warn, msg: "Found user? #{user.present?}. Valid password?[#{user&.valid_password?(password)}]")
16
+ context.fail!(message: "Incorrect credentials. Please try again")
17
+ end
18
+ end
19
+
20
+ def validate!
21
+ raise "Expected email to be a String. Received #{email.class}" unless email.is_a? String
22
+ raise "Expected password to be a String. Received #{password.class}" unless password.is_a? String
23
+
24
+ return unless current_user
25
+ raise "Expected current_user to be a User. Received #{current_user.class}" unless current_user.is_a? User
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ module RailsBase::Authentication
2
+ module Constants
3
+ # Shared
4
+ URL_HELPER = RailsBase.url_routes
5
+ BASE_URL = RailsBase.config.app.base_url
6
+ BASE_URL_PORT = RailsBase.config.app.base_port
7
+ MFA_REASON = :two_factor_mfa_code
8
+ MFA_LENGTH = RailsBase.config.mfa.mfa_length
9
+ EMAIL_LENGTH = 255 # MAX LENGTH we can insert into mysql
10
+
11
+ MIN_NAME = 2
12
+ MAX_NAME = 25
13
+ NAME_VALIDATION = "Must be #{MIN_NAME} to #{MAX_NAME} in length. Can contain characters [a-zA-Z ']"
14
+
15
+ # verify forgot password
16
+ VFP_PURPOSE = :forgot_password_flow
17
+ VFP_REASON = :email_sso_forgot_password
18
+
19
+ # mfa set encrypt token
20
+ MSET_PURPOSE = :mfa_session_token
21
+
22
+ # send login mfa to user
23
+ SLMTU_TTL = (5.minutes + 30.seconds)
24
+
25
+ # send verification email
26
+ SVE_TTL = 1.hour || 7.minutes
27
+ SVE_LOGIN_REASON = :email_sso_login_verify
28
+ SVE_FORGOT_REASON = :email_sso_forgot_password
29
+
30
+ # mfa validator
31
+ MV_BASE_NAME = 'mfa_pos_'
32
+ MV_FISHY = 'Kicked back to login. You are doing something fishy.'
33
+
34
+ # sso verifiy email
35
+ SSOVE_PURPOSE = :verify_email
36
+
37
+ # modify password
38
+ MP_MIN_LENGTH = 7
39
+ MP_MIN_NUMS = 1
40
+ MP_MIN_ALPHA = 6
41
+ var = []
42
+ var << "contain at least #{MP_MIN_NUMS} numerics [0-9]" if MP_MIN_NUMS > 0
43
+ var << "contain at least #{MP_MIN_ALPHA} letters [a-z,A-Z]" if MP_MIN_NUMS > 0
44
+ MP_REQ_MESSAGE = "Password must #{var.join(' and ')}. Minimum length is #{MP_MIN_LENGTH} and contain [1-9a-zA-Z] only"
45
+
46
+ STATIC_WAIT_FLASH = '"Check email inbox for verification email. Follow instructions to gain access"'
47
+
48
+ # SSO LOGIN Reason
49
+ SSO_LOGIN_REASON = 'sso_login_data'
50
+
51
+ ADMIN_REMEMBER_REASON = 'current_admin_user'
52
+ ADMIN_REMEMBER_USERID_KEY = 'admin_remember_me_via_coco'
53
+ ADMIN_MAX_IDLE_TIME = 3.minutes
54
+
55
+ SSO_SEND_LENGTH = 64
56
+ SSO_SEND_USES = 2
57
+ SSO_REASON = :sending_sso_to_user
58
+ SSO_EXPIRES = 2.hours
59
+ end
60
+ end
@@ -0,0 +1,76 @@
1
+ module RailsBase::Authentication
2
+ class DecisionTwofaType < RailsBase::ServiceBase
3
+ delegate :user, to: :context
4
+
5
+ include ActionView::Helpers::DateHelper
6
+
7
+ def call
8
+ # default return values
9
+ context.set_mfa_randomized_token = false
10
+ context.sign_in_user = false
11
+
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
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)
30
+ end
31
+
32
+ log(level: :info, msg: "User #{user.id}: redirect_url: #{context.redirect_url}, sign_in_user: #{context.sign_in_user}, flash: #{context.flash}")
33
+ end
34
+
35
+ def validate_email_context!
36
+ # user has signed up but have not validated their email
37
+ context.redirect_url = Constants::URL_HELPER.auth_static_path
38
+ context.set_mfa_randomized_token = true
39
+ context.mfa_purpose = Constants::SSOVE_PURPOSE
40
+ context.flash = { notice: Constants::STATIC_WAIT_FLASH }
41
+ context.token_ttl = Time.zone.now + 5.minutes
42
+ SendVerificationEmail.call(user: user, reason: Constants::SVE_LOGIN_REASON)
43
+ end
44
+
45
+ def sign_in_user_context!
46
+ log(level: :warn, msg: "Will log in user #{user.id} and bypass 2fa")
47
+ context.redirect_url = Constants::URL_HELPER.authenticated_root_path
48
+ context.sign_in_user = true
49
+ end
50
+
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)
61
+ context.token_ttl = result.short_lived_data.death_time if result.success?
62
+ result
63
+ else
64
+ 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}")
68
+ nil
69
+ end
70
+ end
71
+
72
+ def validate!
73
+ raise "Expected user to be a User. Received #{user.class}" unless user.is_a? User
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,45 @@
1
+ module RailsBase::Authentication
2
+ class DestroyUser < RailsBase::ServiceBase
3
+ delegate :current_user, to: :context
4
+ delegate :data, to: :context
5
+
6
+ include RailsBase::UserSettingsHelper
7
+
8
+ def call
9
+ datum = get_short_lived_datum(data)
10
+ validate_datum?(datum)
11
+ # sign_out(current_user)
12
+ destroy_user!
13
+ end
14
+
15
+ def destroy_user!
16
+ log(level: :warn, msg: "Destroying user: #{current_user.id}")
17
+
18
+ # delete the user
19
+ current_user.soft_destroy_user!
20
+ end
21
+
22
+ def validate_datum?(datum)
23
+ return true if datum[:valid]
24
+
25
+ if datum[:found]
26
+ msg = "Errors with Destroy User token: #{datum[:invalid_reason].join(", ")}. Please try again"
27
+ log(level: :warn, msg: msg)
28
+ context.fail!(message: msg)
29
+ end
30
+
31
+ log(level: :warn, msg: "Could not find datum code. User may be doing Fishyyyyy things")
32
+
33
+ context.fail!(message: "Invalid Data Code. Please retry action")
34
+ end
35
+
36
+ def get_short_lived_datum(data)
37
+ ShortLivedData.find_datum(data: data, reason: DATUM_REASON)
38
+ end
39
+
40
+ def validate!
41
+ raise "Expected data to be a String. Received #{data.class}" unless data.is_a? String
42
+ raise "Expected current_user to be a User. Received #{current_user.class}" unless current_user.is_a? User
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module RailsBase::Authentication
2
+ class MfaSetEncryptToken < RailsBase::ServiceBase
3
+ delegate :user, to: :context
4
+ delegate :expires_at, to: :context
5
+ delegate :purpose, to: :context
6
+
7
+ def call
8
+ params = {
9
+ value: value,
10
+ purpose: purpose || Constants::MSET_PURPOSE,
11
+ expires_at: expires_at
12
+ }
13
+
14
+ context.encrypted_val = RailsBase::Encryption.encode(params)
15
+ end
16
+
17
+ def value
18
+ # user_id with the same expires_at will return the same Encryption token
19
+ # to overcome this, do 2 things
20
+ # 1: Rotate the secret on every boot (ensures tplem changes on semi regular basis)
21
+ # 2: Add rand strings to the hash -- Ensures the token is different every time
22
+ { user_id: user.id, rand: rand.to_s, expires_at: expires_at }.to_json
23
+ end
24
+
25
+ def validate!
26
+ raise "Expected user to be a User. Received #{user.class}" unless user.is_a? User
27
+
28
+ time_class = ActiveSupport::TimeWithZone
29
+ raise "Expected expires_at to be a Received #{time_class}. Received #{expires_at.class}" unless expires_at.is_a? time_class
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,88 @@
1
+ module RailsBase::Authentication
2
+ class MfaValidator < RailsBase::ServiceBase
3
+ delegate :params, to: :context
4
+ delegate :session_mfa_user_id, to: :context
5
+ delegate :current_user, to: :context
6
+ delegate :input_reason, to: :context
7
+
8
+ def call
9
+ array = convert_to_array
10
+ if array.length != Constants::MFA_LENGTH
11
+ log(level: :warn, msg: "Not enough params for MFA code. Given #{array}. Expected of length #{Constants::MFA_LENGTH}")
12
+ context.fail!(message: Constants::MV_FISHY, redirect_url: Constants::URL_HELPER.new_user_session_path, level: :alert)
13
+ end
14
+
15
+ mfa_code = array.join
16
+ log(level: :info, msg: "mfa code received: #{mfa_code}")
17
+ datum = get_short_lived_datum(mfa_code)
18
+ log(level: :info, msg: "Datum returned with: #{datum}")
19
+
20
+ validate_datum?(datum)
21
+ validate_user_consistency?(datum)
22
+ validate_current_user?(datum) if current_user
23
+
24
+ context.user = datum[:user]
25
+ end
26
+
27
+ def validate_current_user?(datum)
28
+ return true if current_user.id == datum[:user].id
29
+
30
+ # User MFA for a different user matched the session token
31
+ # However, those did not match the current user signed in
32
+ # Something is very 🐟
33
+ log(level: :error, msg: "Someone is a teapot. Current logged in user does not equal mfa code.")
34
+ context.fail!(message: 'You are a teapot', redirect_url: Constants::URL_HELPER.signout_path, level: :warn)
35
+ end
36
+
37
+ def validate_datum?(datum)
38
+ return true if datum[:valid]
39
+
40
+ if datum[:found]
41
+ # MFA is either expired or the incorrect reason. Either way it does not match
42
+ msg = "Errors with MFA: #{datum[:invalid_reason].join(", ")}. Please login again"
43
+ log(level: :warn, msg: msg)
44
+ context.fail!(message: msg, redirect_url: Constants::URL_HELPER.new_user_session_path, level: :warn)
45
+ end
46
+
47
+ # MFA does not exist for any reason type
48
+ log(level: :warn, msg: "Could not find MFA code. Incorrect MFA code")
49
+
50
+ context.fail!(message: "Incorrect MFA code.", redirect_url: Constants::URL_HELPER.mfa_code_path, level: :warn)
51
+ end
52
+
53
+ def validate_user_consistency?(datum)
54
+ return true if datum[:user].id == session_mfa_user_id.to_i
55
+ log(level: :warn, msg: "Datum user does not match session user. [#{datum[:user].id}, #{session_mfa_user_id.to_i}]")
56
+
57
+ # MFA session token user does not match the datum user
58
+ # Something is very 🐟
59
+ context.fail!(message: Constants::MV_FISHY, redirect_url: Constants::URL_HELPER.new_user_session_path, level: :alert)
60
+ end
61
+
62
+ def get_short_lived_datum(mfa_code)
63
+ log(level: :debug, msg: "Looking for #{mfa_code} with reason #{reason}")
64
+ ShortLivedData.find_datum(data: mfa_code, reason: reason)
65
+ end
66
+
67
+ def convert_to_array
68
+ array = []
69
+ return array unless params.dig(:mfa).respond_to? :keys
70
+
71
+ Constants::MFA_LENGTH.times do |index|
72
+ var_name = "#{Constants::MV_BASE_NAME}#{index}".to_sym
73
+ array << params[:mfa][var_name]
74
+ end
75
+
76
+ array.compact
77
+ end
78
+
79
+ def reason
80
+ input_reason || Constants::MFA_REASON
81
+ end
82
+
83
+ def validate!
84
+ raise 'Expected the params passed' if params.nil?
85
+ raise 'session_mfa_user_id is not present' if session_mfa_user_id.nil?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,67 @@
1
+ module RailsBase::Authentication
2
+ class ModifyPassword < RailsBase::ServiceBase
3
+ include RailsBase::UserFieldValidators
4
+
5
+ delegate :password, to: :context
6
+ delegate :password_confirmation, to: :context
7
+ delegate :data, to: :context # the same datum used in the reset password url
8
+ delegate :user_id, to: :context
9
+ delegate :flow, to: :context
10
+ delegate :current_user, to: :context
11
+
12
+ FLOW_TYPES = [:forgot_password, :user_settings]
13
+
14
+ def call
15
+ valid_password = validate_password?(password: password, password_confirmation: password_confirmation)
16
+ unless valid_password[:status]
17
+ context.fail!(message: valid_password[:msg])
18
+ end
19
+
20
+ if user.nil?
21
+ log(level: :error, msg: "Could not find [#{user_id}]. 5xx error with user deletion most likely")
22
+ context.fail!(message: "Unknown error. Please try again")
23
+ end
24
+
25
+ case flow
26
+ when :forgot_password
27
+ forgot_password
28
+ else
29
+ end
30
+
31
+ unless user.update(password: password, password_confirmation: password_confirmation)
32
+ context.fail!(message: "Failed to update user. Please try again")
33
+ end
34
+ log(level: :info, msg: "Successfully update users password")
35
+ end
36
+
37
+ def forgot_password
38
+ return if valid_short_term_data_point?
39
+
40
+ context.fail!(message: Constants::MV_FISHY)
41
+ end
42
+
43
+ def valid_short_term_data_point?
44
+ raise 'Expected data to be defined' if data.nil?
45
+
46
+ data_point = ShortLivedData.get_by_data(data: data, reason: Constants::VFP_REASON)
47
+ datum_user_id = data_point&.user_id
48
+
49
+ log(level: :info, msg: "Found ShortLivedData with data #{data[0..15]}... attached to user [#{datum_user_id}]")
50
+
51
+ datum_user_id && (datum_user_id.to_i == user_id.to_i)
52
+ end
53
+
54
+ def user
55
+ @user ||= current_user || User.find_by_id(user_id)
56
+ end
57
+
58
+ def validate!
59
+ raise "Expected Flow to be present and a Symbol" unless flow.is_a? Symbol
60
+ raise "Expected Flow to be in #{FLOW_TYPES}" unless FLOW_TYPES.include?(flow)
61
+ raise "Expected password to be a String. Received #{password.class}" unless password.is_a? String
62
+ raise "Expected password_confirmation to be a String. Received #{password_confirmation.class}" unless password_confirmation.is_a? String
63
+ raise "Expected user_id to be present." if current_user.nil? && user_id.nil?
64
+ raise "Expected data to be present." if current_user.nil? && data.nil?
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,26 @@
1
+ module RailsBase::Authentication
2
+ class SendForgotPassword < RailsBase::ServiceBase
3
+ delegate :email, to: :context
4
+
5
+ def call
6
+ user = User.find_for_authentication(email: email)
7
+
8
+ if user.nil?
9
+ log(level: :warn, msg: "Failed to find email assocaited to #{email}. Not sending email")
10
+ context.fail!(message: "Failed to send forget password to #{email}", redirect_url: '')
11
+ end
12
+ email_send = SendVerificationEmail.call(user: user, reason: Constants::VFP_REASON)
13
+
14
+ if email_send.failure?
15
+ log(level: :error, msg: "Failed to send forget password: #{email_send.message}")
16
+ context.fail!(message: email_send.message, redirect_url: '/')
17
+ end
18
+
19
+ context.message = 'You should receive an email shortly.'
20
+ end
21
+
22
+ def validate!
23
+ raise "Expected email to be a String. Received #{email.class}" unless email.is_a? String
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,77 @@
1
+ require 'twilio_helper'
2
+ require 'velocity_limiter'
3
+
4
+ module RailsBase::Authentication
5
+ class SendLoginMfaToUser < RailsBase::ServiceBase
6
+ include ActionView::Helpers::DateHelper
7
+ include VelocityLimiter
8
+
9
+ class NoPhoneNumber < StandardError; end
10
+
11
+ MAX_USE_COUNT = 1.freeze
12
+ DATA_USE = :numeric
13
+
14
+ delegate :user, to: :context
15
+ delegate :expires_at, to: :context
16
+
17
+ def call
18
+ velocity = velocity_limit_reached?
19
+ context.fail!(message: velocity[:msg]) if velocity[:reached]
20
+
21
+ data_point = create_short_lived_data
22
+ send_twilio!(data_point.data)
23
+ context.short_lived_data = data_point
24
+ end
25
+
26
+ 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
+ rescue StandardError => e
30
+ log(level: :error, msg: "Error caught #{e.class.name}")
31
+ log(level: :error, msg: "Failed to send sms to #{user.phone_number}")
32
+ context.fail!(message: "Failed to send sms. Please retry logging in.")
33
+ end
34
+
35
+ def message(code)
36
+ "Hello #{user.full_name}. Here is your verification code #{code}."
37
+ end
38
+
39
+ def create_short_lived_data
40
+ params = {
41
+ user: user,
42
+ max_use: MAX_USE_COUNT,
43
+ reason: Constants::MFA_REASON,
44
+ data_use: DATA_USE,
45
+ ttl: Constants::SLMTU_TTL,
46
+ expires_at: expires_at,
47
+ length: Constants::MFA_LENGTH,
48
+ }
49
+ ShortLivedData.create_data_key(params)
50
+ end
51
+
52
+ def velocity_max_in_frame
53
+ RailsBase.config.mfa.twilio_velocity_max_in_frame
54
+ end
55
+
56
+ def velocity_max
57
+ RailsBase.config.mfa.twilio_velocity_max
58
+ end
59
+
60
+ def velocity_frame
61
+ RailsBase.config.mfa.twilio_velocity_frame
62
+ end
63
+
64
+ def cache_key
65
+ "#{self.class.name.downcase}.#{user.id}"
66
+ end
67
+
68
+ def validate!
69
+ raise "Expected user to be a User. Received #{user.class}" unless user.is_a? User
70
+ if expires_at && !(expires_at.is_a?(ActiveSupport::TimeWithZone))
71
+ raise "Expected expires_at to be a ActiveSupport::TimeWithZone. Given #{expires_at.class}"
72
+ end
73
+
74
+ raise NoPhoneNumber, "No phone for user [#{user.id}] [#{user.phone_number}]" if user.phone_number.nil?
75
+ end
76
+ end
77
+ end