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
@@ -7,6 +7,16 @@ module RailsBase
7
7
 
8
8
  # GET user/settings
9
9
  def index
10
+ @type = :rest
11
+ @endpoint = RailsBase.url_routes.totp_register_validate_path
12
+
13
+ if current_user.mfa_sms_enabled
14
+ clear_mfa_event_from_session!(event_name: RailsBase::MfaEvent::ENABLE_SMS_EVENT)
15
+ add_mfa_event_to_session(event: RailsBase::MfaEvent.sms_disable(user: current_user))
16
+ else
17
+ clear_mfa_event_from_session!(event_name: RailsBase::MfaEvent::DISABLE_SMS_EVENT)
18
+ add_mfa_event_to_session(event: RailsBase::MfaEvent.sms_enable(user: current_user))
19
+ end
10
20
  end
11
21
 
12
22
  # POST user/settings/edit/name
@@ -14,7 +24,7 @@ module RailsBase
14
24
  result = NameChange.call(
15
25
  first_name: params[:user][:first_name],
16
26
  last_name: params[:user][:last_name],
17
- current_user: current_user
27
+ current_user: current_user,
18
28
  )
19
29
 
20
30
  if result.failure?
@@ -50,7 +50,7 @@ class RailsBase::Users::RegistrationsController < Devise::RegistrationsControlle
50
50
  end
51
51
  session[:mfa_randomized_token] = nil
52
52
  session[:mfa_randomized_token] =
53
- RailsBase::Authentication::MfaSetEncryptToken.call(purpose: RailsBase::Authentication::Constants::SSOVE_PURPOSE, user: resource, expires_at: Time.zone.now + 20.minutes).encrypted_val
53
+ RailsBase::Mfa::EncryptToken.call(purpose: RailsBase::Authentication::Constants::SSOVE_PURPOSE, user: resource, expires_at: Time.zone.now + 20.minutes).encrypted_val
54
54
  redirect_to RailsBase.url_routes.auth_static_path, notice: "Check email for verification email."
55
55
  else
56
56
  flash[:error] = resource.errors.messages
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'twilio_helper'
4
+
3
5
  class RailsBase::Users::SessionsController < Devise::SessionsController
4
6
  prepend_before_action :protect_heartbeat, only: [:hearbeat_without_auth, :hearbeat_with_auth]
5
7
  prepend_before_action :skip_timeout, only: [:hearbeat_without_auth]
@@ -13,7 +15,7 @@ class RailsBase::Users::SessionsController < Devise::SessionsController
13
15
  # POST /user/sign_in
14
16
  def create
15
17
  # Warden/Devise will try to sign the user in before we explicitly do
16
- # Sign ou the user when this happens so we can sign them back in later
18
+ # Sign out the user when this happens so we can sign them back in later
17
19
  sign_out(current_user) if current_user
18
20
 
19
21
  authenticate = RailsBase::Authentication::AuthenticateUser.call(email: params[:user][:email], password: params[:user][:password])
@@ -33,25 +35,26 @@ class RailsBase::Users::SessionsController < Devise::SessionsController
33
35
 
34
36
  if mfa_decision.set_mfa_randomized_token
35
37
  session[:mfa_randomized_token] =
36
- RailsBase::Authentication::MfaSetEncryptToken.call(
38
+ RailsBase::Mfa::EncryptToken.call(
37
39
  user: authenticate.user,
38
40
  expires_at: mfa_decision.token_ttl,
39
41
  purpose: mfa_decision.mfa_purpose,
40
42
  ).encrypted_val
41
43
  end
42
44
 
43
- redirect =
44
- if mfa_decision.sign_in_user
45
- sign_in(authenticate.user)
46
- # only referentially redirect when we know the user should sign in
47
- redirect_from_reference
48
- end
49
-
50
- redirect ||= mfa_decision.redirect_url
51
-
52
- logger.info { "Successful sign in: Redirecting to #{redirect}" }
45
+ if mfa_decision.sign_in_user
46
+ sign_in(authenticate.user)
47
+ session.merge!(mfa_decision.session || {})
48
+ # only referentially redirect when we know the user should sign in
49
+ redirect_to(redirect_from_reference || RailsBase.url_routes.authenticated_root_path, mfa_decision.flash)
50
+ return
51
+ end
53
52
 
54
- redirect_to(redirect, mfa_decision.flash)
53
+ ####
54
+ # User needs MFA
55
+ ####
56
+ add_mfa_event_to_session(event: RailsBase::MfaEvent.login_event(user: authenticate.user))
57
+ redirect_to(mfa_decision.redirect_url, mfa_decision.flash)
55
58
  end
56
59
 
57
60
  # DELETE /user/sign_out
@@ -6,6 +6,7 @@ class RailsBaseApplicationController < ActionController::Base
6
6
  before_action :is_timeout_error?
7
7
  before_action :admin_reset_impersonation_session!
8
8
  before_action :footer_mode_case
9
+ after_action :clear_expired_mfa_events_from_session!
9
10
 
10
11
  before_action :populate_admin_actions, if: -> { RailsBase.config.admin.enable_actions? }
11
12
  after_action :capture_admin_action
@@ -128,6 +129,100 @@ class RailsBaseApplicationController < ActionController::Base
128
129
 
129
130
  protected
130
131
 
132
+ def add_mfa_event_to_session(event:)
133
+ unless RailsBase::MfaEvent === event
134
+ logger.error("Failed to add MFA event to session. Unexpected event passed")
135
+ return false
136
+ end
137
+
138
+ session[:"__#{RailsBase.app_name}_mfa_events"] ||= {}
139
+ # nested hashes in the session are string keys -- ensure it gets converted to a string during assignment to avoid confusion
140
+ session[:"__#{RailsBase.app_name}_mfa_events"][event.event.to_s] = event.to_hash.to_json
141
+ end
142
+
143
+ def clear_mfa_event_from_session!(event_name:)
144
+ session[:"__#{RailsBase.app_name}_mfa_events"] ||= {}
145
+ session[:"__#{RailsBase.app_name}_mfa_events"].delete(event_name.to_s)
146
+
147
+ true
148
+ end
149
+
150
+ def clear_expired_mfa_events_from_session!
151
+ mfa_events = session.delete(:"__#{RailsBase.app_name}_mfa_events")
152
+ return true if mfa_events.nil?
153
+
154
+ mfa_events.each do |event_name, metadata|
155
+ event_object = RailsBase::MfaEvent.new(**JSON.parse(metadata).deep_symbolize_keys)
156
+ if event_object.valid_by_death_time?
157
+ add_mfa_event_to_session(event: event_object)
158
+ else
159
+ logger.warn("MFA event: [#{event_name}] is no longer valid. Ejecting from session ")
160
+ end
161
+
162
+ end
163
+ rescue => e
164
+ logger.error("Oh know! We may have just removed all MFA events. Re-Auth is now required")
165
+ true
166
+ end
167
+
168
+ def validate_mfa_with_event_json!(mfa_event_name: params[:mfa_event])
169
+ return true if soft_mfa_with_event(mfa_event_name:)
170
+
171
+ render json: { error: "Unauthorized. Incorrect Event" }.to_json, :status => 401
172
+ false
173
+ end
174
+
175
+ def validate_mfa_with_event!(mfa_event_name: params[:mfa_event])
176
+ return true if soft_mfa_with_event(mfa_event_name:)
177
+
178
+ redirect = @__rails_base_mfa_event&.invalid_redirect || RailsBase.url_routes.unauthenticated_root_path
179
+ redirect_to(redirect, alert: @__rails_base_mfa_event_invalid_reason)
180
+ false
181
+ end
182
+
183
+ def soft_mfa_with_event(mfa_event_name: params[:mfa_event])
184
+ # nested hashes in the session are string keys -- ensure it gets converted to a string during lookup
185
+ mfa_event = session.dig(:"__#{RailsBase.app_name}_mfa_events", mfa_event_name.to_s)
186
+ if mfa_event.nil?
187
+ @__rails_base_mfa_event_invalid_reason = "Unauthorized MFA event"
188
+ return false
189
+ end
190
+ @__rails_base_mfa_event = RailsBase::MfaEvent.new(**JSON.parse(mfa_event).deep_symbolize_keys)
191
+
192
+ if @__rails_base_mfa_event.valid?
193
+ @__rails_base_mfa_event.increase_access_count!
194
+ return true
195
+ end
196
+ @__rails_base_mfa_event_invalid_reason = "MFA event for #{mfa_event_name} is invalid. #{@__rails_base_mfa_event.event}"
197
+
198
+ false
199
+ end
200
+
201
+ def json_validate_current_user!
202
+ return if current_user
203
+
204
+ render json: { error: "Unauthorized" }.to_json, :status => 401
205
+ return false
206
+ end
207
+
208
+ def validate_mfa_token!(purpose: RailsBase::Authentication::Constants::MSET_PURPOSE)
209
+ return true if soft_validate_mfa_token(token: session[:mfa_randomized_token], purpose: purpose)
210
+
211
+ if user_signed_in?
212
+ redirect_to RailsBase.url_routes.user_settings_path, alert: @token_verifier.message
213
+ else
214
+ redirect_to RailsBase.url_routes.new_user_session_path, alert: @token_verifier.message
215
+ end
216
+ return false
217
+ end
218
+
219
+ def soft_validate_mfa_token(token:, purpose: RailsBase::Authentication::Constants::MSET_PURPOSE)
220
+ @token_verifier =
221
+ RailsBase::Authentication::SessionTokenVerifier.call(purpose: purpose, mfa_randomized_token: token)
222
+
223
+ @token_verifier.success?
224
+ end
225
+
131
226
  def admin_get_token(encrypted_val:)
132
227
  params = {
133
228
  mfa_randomized_token: encrypted_val,
@@ -144,7 +239,7 @@ class RailsBaseApplicationController < ActionController::Base
144
239
  purpose: RailsBase::Authentication::Constants::ADMIN_REMEMBER_REASON,
145
240
  expires_at: RailsBase::Authentication::Constants::ADMIN_MAX_IDLE_TIME.from_now
146
241
  }
147
- encrpytion = RailsBase::Authentication::MfaSetEncryptToken.call(params)
242
+ encrpytion = RailsBase::Mfa::EncryptToken.call(params)
148
243
  session[RailsBase::Authentication::Constants::ADMIN_REMEMBER_REASON] = encrpytion.encrypted_val
149
244
  end
150
245
  end
@@ -1,7 +1,7 @@
1
1
  require 'twilio_helper'
2
2
 
3
3
  class TwilioJob < RailsBase::ApplicationJob
4
- queue_as RailsBase.config.mfa.active_job_queue
4
+ queue_as RailsBase.config.twilio.active_job_queue
5
5
 
6
6
  def perform(message:, to:)
7
7
  TwilioHelper.send_sms(message: message, to: to)
@@ -1,22 +1,24 @@
1
1
  class RailsBase::EmailVerificationMailer < RailsBase::ApplicationMailer
2
2
  default from: Rails.configuration.mail_from
3
3
 
4
- def email_verification(user:, url:)
4
+ def email_verification(user, url)
5
5
  @user = user
6
6
  @sso_url_for_user = url
7
7
  mail(to: @user.email, subject: "Welcome to #{RailsBase.app_name}")
8
8
  end
9
9
 
10
- def forgot_password(user:, url:)
10
+ def forgot_password(user, url)
11
11
  @user = user
12
12
  @sso_url_for_user = url
13
13
  mail(to: @user.email, subject: "#{RailsBase.app_name}: Forgot Password")
14
14
  end
15
15
 
16
- def event(user:, event:, msg: nil)
17
- @user = user
16
+ def event(user, event, msg = nil)
17
+ @user = user
18
18
  @event = event
19
19
  @msg = msg
20
20
  mail(to: @user.email, subject: "#{RailsBase.app_name}: #{event}")
21
21
  end
22
+
23
+ include ::RailsBase::MailerKwargInject
22
24
  end
@@ -1,16 +1,18 @@
1
1
  class RailsBase::EventMailer < RailsBase::ApplicationMailer
2
2
  default from: Rails.configuration.mail_from
3
3
 
4
- def send_sso(user:, message:)
4
+ def send_sso(user, message)
5
5
  @user = user
6
6
  @message = message
7
7
  mail(to: user.email, subject: "#{RailsBase.app_name}: SSO login", template_name: 'event')
8
8
  # event(user: user, event: 'SSO login', message: message)
9
9
  end
10
10
 
11
- def event(user:, event:, message:)
11
+ def event(user, event, message)
12
12
  @user = user
13
13
  @message = message
14
14
  mail(to: @user.email, subject: "#{RailsBase.app_name}: #{event}", template_name: 'event')
15
15
  end
16
+
17
+ include ::RailsBase::MailerKwargInject
16
18
  end
@@ -0,0 +1,31 @@
1
+ module RailsBase::MailerKwargInject
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ base.inject_safety_net!
6
+ end
7
+
8
+ module ClassMethods
9
+ def inject_safety_net!
10
+ action_methods.each do |method_name|
11
+ alias_method_name = "__rails_base__kwarg_inject_#{method_name}__"
12
+ self.alias_method(alias_method_name, method_name)
13
+
14
+ self.define_method(method_name) do |*args, **kwargs, &block|
15
+ parameter_name_order = method(alias_method_name).parameters.map(&:second)
16
+ begin
17
+ self.send(alias_method_name, *args, **kwargs, &block)
18
+ rescue ArgumentError => e
19
+ if Hash === args[0]
20
+ new_arg_list = parameter_name_order.map { args[0][_1] }
21
+ self.send(alias_method_name, *new_arg_list, &block)
22
+ ActiveSupport::Deprecation.warn("Method Signature of `#{self.class}.#{method_name}` will change from KWargs to ARGs. Please modify your code.")
23
+ else
24
+ raise
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -8,9 +8,11 @@ module RailsBase
8
8
  ]
9
9
 
10
10
  SOFT_DESTROY_PARAMS = {
11
- mfa_enabled: false,
11
+ mfa_sms_enabled: false,
12
+ mfa_otp_enabled: false,
12
13
  email_validated: false,
13
- last_mfa_login: nil,
14
+ last_mfa_sms_login: nil,
15
+ last_mfa_otp_login: nil,
14
16
  encrypted_password: '',
15
17
  phone_number: nil,
16
18
  }
@@ -20,7 +22,8 @@ module RailsBase
20
22
  admin: ->(user) { RailsBase.config.admin.admin_type_tile_users?(user) } ,
21
23
  email: ->(user) { RailsBase.config.admin.email_tile_users?(user) } ,
22
24
  email_validated: ->(user) { RailsBase.config.admin.email_validate_tile_users?(user) } ,
23
- mfa_enabled: ->(user) { RailsBase.config.admin.mfa_tile_users?(user) } ,
25
+ mfa_sms_enabled: ->(user) { RailsBase.config.admin.mfa_tile_users?(user) } ,
26
+ mfa_otp_enabled: ->(user) { RailsBase.config.admin.mfa_tile_users?(user) } ,
24
27
  phone_number: ->(user) { RailsBase.config.admin.phone_tile_users?(user) } ,
25
28
  last_known_timezone: ->(user) { RailsBase.config.admin.modify_timezone_tile_users?(user) }
26
29
  }
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsBase
4
+ module UserHelper
5
+ module Totp
6
+ module BackupMethodOptions
7
+ def generate_otp_backup_codes!
8
+ codes = User.generate_backup_codes
9
+ self.otp_backup_codes = codes
10
+ save!
11
+
12
+ codes
13
+ end
14
+
15
+ def invalidate_otp_backup_code!(code)
16
+ codes = self.otp_backup_codes || []
17
+
18
+ return false unless codes.include?(code)
19
+
20
+ codes.delete(code)
21
+
22
+ self.otp_backup_codes = codes
23
+
24
+ save!
25
+ end
26
+
27
+ def totp_config
28
+ RailsBase.config.totp
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsBase
4
+ module UserHelper
5
+ module Totp
6
+ module ClassOptions
7
+ def totp_drift_ahead
8
+ RailsBase.config.totp.allowed_drift_ahead || RailsBase.config.totp.allowed_drift
9
+ end
10
+
11
+ def totp_drift_behind
12
+ RailsBase.config.totp.allowed_drift_behind || RailsBase.config.totp.allowed_drift
13
+ end
14
+
15
+ def generate_otp_secret(otp_secret_length = RailsBase.config.totp.secret_code_length)
16
+ ROTP::Base32.random_base32(otp_secret_length)
17
+ end
18
+
19
+ def generate_backup_codes
20
+ number_of_codes = RailsBase.config.totp.backup_code_count
21
+ code_length = RailsBase.config.totp.backup_code_length
22
+ codes = []
23
+ number_of_codes.times do
24
+ codes << SecureRandom.hex(code_length / 2) # Hexstring has length 2*n
25
+ end
26
+
27
+ codes
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rqrcode"
4
+
5
+ module RailsBase
6
+ module UserHelper
7
+ module Totp
8
+ module ConsumeMethodOptions
9
+ def persist_otp_metadata!
10
+ reload # Ensure the user is relaoded to use the correct data for the secret
11
+ metadata = otp_metadata(safe: true, use_existing_temp: true)
12
+
13
+ self.otp_secret = self.otp_secret || self.temp_otp_secret
14
+ self.temp_otp_secret = nil
15
+ self.mfa_otp_enabled = true
16
+ save!
17
+ end
18
+
19
+ def otp_provisioning_uri(options = {})
20
+ label = options.delete(:label) || "#{RailsBase.app_name}:#{self.email}"
21
+ otp_secret = options[:otp_secret] || self.otp_secret
22
+
23
+ ROTP::TOTP.new(otp_secret, options).provisioning_uri(label)
24
+ end
25
+
26
+ def otp_metadata(safe: false, use_existing_temp: false)
27
+ secret ||= self.otp_secret
28
+ secret ||= self.temp_otp_secret if safe && use_existing_temp
29
+ secret ||= temporary_otp! if safe
30
+
31
+ uri = otp_provisioning_uri({ otp_secret: secret })
32
+ { secret: secret, uri: uri, qr_code: qr_code(uri) }
33
+ end
34
+
35
+ protected
36
+
37
+ def qr_code(uri)
38
+ qrcode = RQRCode::QRCode.new(uri)
39
+ qrcode.as_svg(
40
+ color: "000",
41
+ shape_rendering: "crispEdges",
42
+ module_size: 4,
43
+ standalone: true,
44
+ use_path: true
45
+ )
46
+ end
47
+
48
+ def temporary_otp!(otp_secret_length = RailsBase.config.totp.secret_code_length)
49
+ otp_secret = User.generate_otp_secret(otp_secret_length)
50
+
51
+ self.temp_otp_secret = otp_secret
52
+ save!
53
+
54
+ otp_secret
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rotp"
4
+
5
+ ####
6
+ #
7
+ # Copied with love from https://github.com/devise-two-factor/devise-two-factor
8
+ #
9
+ ####
10
+
11
+ module RailsBase
12
+ module UserHelper
13
+ module Totp
14
+ extend ActiveSupport::Concern
15
+
16
+ class Error < StandardError; end
17
+ class NotRequired < Error; end
18
+
19
+ included do
20
+ serialize :otp_backup_codes, Array
21
+ end
22
+
23
+ def self.included(base)
24
+ base.include(ConsumeMethodOptions)
25
+ base.include(BackupMethodOptions)
26
+ base.extend(ClassOptions)
27
+ end
28
+
29
+ def reset_otp!
30
+ self.otp_secret = nil
31
+ self.temp_otp_secret = nil
32
+ self.consumed_timestep = nil
33
+ self.mfa_otp_enabled = false
34
+ self.otp_backup_codes = []
35
+ self.last_mfa_otp_login = nil
36
+
37
+ save!
38
+ end
39
+ end
40
+ end
41
+ end
data/app/models/user.rb CHANGED
@@ -6,9 +6,9 @@
6
6
  # first_name :string(255) default(""), not null
7
7
  # last_name :string(255) default(""), not null
8
8
  # phone_number :string(255)
9
- # last_mfa_login :datetime
9
+ # last_mfa_sms_login :datetime
10
10
  # email_validated :boolean default(FALSE)
11
- # mfa_enabled :boolean default(FALSE), not null
11
+ # mfa_sms_enabled :boolean default(FALSE), not null
12
12
  # active :boolean default(TRUE), not null
13
13
  # admin :string(255)
14
14
  # last_known_timezone :string(255)
@@ -25,6 +25,12 @@
25
25
  # last_sign_in_ip :string(255)
26
26
  # created_at :datetime not null
27
27
  # updated_at :datetime not null
28
+ # otp_secret :string(255)
29
+ # temp_otp_secret :string(255)
30
+ # consumed_timestep :integer
31
+ # mfa_otp_enabled :boolean default(FALSE)
32
+ # otp_backup_codes :text(65535)
33
+ # last_mfa_otp_login :datetime
28
34
  #
29
35
  class User < RailsBase::ApplicationRecord
30
36
  # Include default devise modules. Others available are:
@@ -33,13 +39,14 @@ class User < RailsBase::ApplicationRecord
33
39
  :recoverable, :rememberable, :validatable, :timeoutable, :trackable
34
40
 
35
41
  include RailsBase::UserConstants
42
+ include RailsBase::UserHelper::Totp
36
43
 
37
44
  validate :enforce_owner, if: :will_save_change_to_admin?
38
45
  validate :enforce_admin_type, if: :will_save_change_to_admin?
39
46
 
40
47
  def self._def_admin_convenience_method!(admin_method:)
41
48
  types = RailsBase.config.admin.admin_types
42
- #### metods on the instance
49
+ #### methods on the instance
43
50
  define_method("at_least_#{admin_method}?") do
44
51
  i = types.find_index(admin.to_sym)
45
52
  i >= types.find_index(admin_method.to_sym)
@@ -65,8 +72,16 @@ class User < RailsBase::ApplicationRecord
65
72
  end
66
73
  end
67
74
 
68
- def self.time_bound
69
- Time.zone.now - RailsBase.config.auth.mfa_time_duration
75
+ def self.masked_number(phone_number)
76
+ return nil unless phone_number
77
+
78
+ "(#{phone_number[0]}**) ****-**#{phone_number[-2..-1]}"
79
+ end
80
+
81
+ def self.readable_phone_number(phone_number)
82
+ return nil unless phone_number
83
+
84
+ "(#{phone_number[0..2]}) #{phone_number[3..5]}-#{phone_number[6..-1]}"
70
85
  end
71
86
 
72
87
  def admin
@@ -77,20 +92,20 @@ class User < RailsBase::ApplicationRecord
77
92
  "#{first_name} #{last_name}"
78
93
  end
79
94
 
80
- def past_mfa_time_duration?
81
- return true if last_mfa_login.nil?
82
-
83
- last_mfa_login < self.class.time_bound
95
+ def set_last_mfa_sms_login!(time: Time.zone.now)
96
+ update(last_mfa_sms_login: time)
84
97
  end
85
98
 
86
- def set_last_mfa_login!(time: Time.zone.now)
87
- update(last_mfa_login: time)
99
+ def set_last_mfa_otp_login!(time: Time.zone.now)
100
+ update(last_mfa_otp_login: time)
88
101
  end
89
102
 
90
103
  def masked_phone
91
- return nil unless phone_number
104
+ User.masked_number(phone_number)
105
+ end
92
106
 
93
- "(#{phone_number[0]}**) ****-**#{phone_number[-2..-1]}"
107
+ def readable_phone
108
+ User.readable_phone_number(phone_number)
94
109
  end
95
110
 
96
111
  def soft_destroy_user!
@@ -5,7 +5,7 @@ module RailsBase::Authentication
5
5
  BASE_URL = RailsBase.config.app.base_url
6
6
  BASE_URL_PORT = RailsBase.config.app.base_port
7
7
  MFA_REASON = :two_factor_mfa_code
8
- MFA_LENGTH = RailsBase.config.mfa.mfa_length
8
+ MFA_LENGTH = RailsBase.config.twilio.mfa_length
9
9
  EMAIL_LENGTH = 255 # MAX LENGTH we can insert into mysql
10
10
 
11
11
  MIN_NAME = 2