rails_base 0.75.5 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/rails_base/rails_base_query_checker.js +36 -0
- data/app/controllers/rails_base/admin_controller.rb +54 -9
- data/app/controllers/rails_base/mfa/evaluation_controller.rb +59 -0
- data/app/controllers/rails_base/mfa/register/sms_controller.rb +45 -0
- data/app/controllers/rails_base/mfa/register/totp_controller.rb +42 -0
- data/app/controllers/rails_base/mfa/validate/sms_controller.rb +83 -0
- data/app/controllers/rails_base/mfa/validate/totp_controller.rb +35 -0
- data/app/controllers/rails_base/secondary_authentication_controller.rb +40 -96
- data/app/controllers/rails_base/user_settings_controller.rb +11 -1
- data/app/controllers/rails_base/users/registrations_controller.rb +1 -1
- data/app/controllers/rails_base/users/sessions_controller.rb +16 -13
- data/app/controllers/rails_base_application_controller.rb +96 -1
- data/app/jobs/twilio_job.rb +1 -1
- data/app/mailers/rails_base/email_verification_mailer.rb +6 -4
- data/app/mailers/rails_base/event_mailer.rb +4 -2
- data/app/mailers/rails_base/mailer_kwarg_inject.rb +31 -0
- data/app/models/rails_base/user_constants.rb +6 -3
- data/app/models/rails_base/user_helper/totp/backup_method_options.rb +33 -0
- data/app/models/rails_base/user_helper/totp/class_options.rb +35 -0
- data/app/models/rails_base/user_helper/totp/consume_method_options.rb +60 -0
- data/app/models/rails_base/user_helper/totp.rb +41 -0
- data/app/models/user.rb +28 -13
- data/app/services/rails_base/authentication/constants.rb +1 -1
- data/app/services/rails_base/authentication/decision_twofa_type.rb +61 -30
- data/app/services/rails_base/authentication/send_forgot_password.rb +0 -1
- data/app/services/rails_base/authentication/single_sign_on_send.rb +1 -1
- data/app/services/rails_base/authentication/sso_verify_email.rb +3 -1
- data/app/services/rails_base/authentication/update_phone_send_verification.rb +2 -2
- data/app/services/rails_base/authentication/verify_forgot_password.rb +8 -11
- data/app/services/rails_base/mfa/decision.rb +70 -0
- data/app/services/rails_base/mfa/encrypt_token.rb +34 -0
- data/app/services/rails_base/mfa/sms/remove.rb +35 -0
- data/app/services/rails_base/{authentication/send_login_mfa_to_user.rb → mfa/sms/send.rb} +19 -13
- data/app/services/rails_base/mfa/sms/validate.rb +105 -0
- data/app/services/rails_base/mfa/strategy/base.rb +44 -0
- data/app/services/rails_base/mfa/strategy/every_request.rb +14 -0
- data/app/services/rails_base/mfa/strategy/skip_every_request.rb +14 -0
- data/app/services/rails_base/mfa/strategy/time_based.rb +24 -0
- data/app/services/rails_base/mfa/totp/helper.rb +21 -0
- data/app/services/rails_base/mfa/totp/otp_metadata.rb +19 -0
- data/app/services/rails_base/mfa/totp/remove.rb +40 -0
- data/app/services/rails_base/mfa/totp/validate_code.rb +52 -0
- data/app/services/rails_base/mfa/totp/validate_temporary_code.rb +37 -0
- data/app/services/rails_base/mfa.rb +18 -0
- data/app/services/rails_base/name_change.rb +3 -3
- data/app/views/layouts/rails_base/application.html.erb +22 -6
- data/app/views/rails_base/devise/passwords/new.html.erb +1 -1
- data/app/views/rails_base/mfa/_switch_mfa_type.html.erb +17 -0
- data/app/views/rails_base/mfa/validate/sms/sms_event_input.html.erb +2 -0
- data/app/views/rails_base/mfa/validate/totp/totp_event_input.html.erb +1 -0
- data/app/views/rails_base/secondary_authentication/reset_password_input.html.erb +4 -0
- data/app/views/rails_base/shared/_enable_mfa_auth_modal.html.erb +1 -1
- data/app/views/rails_base/shared/_logged_in_header.html.erb +1 -25
- data/app/views/rails_base/shared/_modify_mfa_auth_modal.html.erb +102 -3
- data/app/views/rails_base/shared/_standardized_collapse.html.erb +28 -0
- data/app/views/rails_base/shared/mfa/sms/_login_input.html.erb +13 -0
- data/app/views/rails_base/shared/mfa/totp/_login_input.html.erb +22 -0
- data/app/views/rails_base/shared/totp/_add_authenticator.html.erb +76 -0
- data/app/views/rails_base/shared/totp/_add_authenticator_modal.html.erb +25 -0
- data/app/views/rails_base/shared/totp/_confirm_code.html.erb +31 -0
- data/app/views/rails_base/shared/totp/_confirm_code_ajax.html.erb +3 -0
- data/app/views/rails_base/shared/totp/_confirm_code_rest.html.erb +5 -0
- data/app/views/rails_base/shared/totp/_remove_authenticator_modal.html.erb +50 -0
- data/app/views/rails_base/user_settings/index.html.erb +84 -1
- data/config/initializers/admin_action_helper.rb +44 -8
- data/config/routes.rb +42 -7
- data/db/migrate/20240808013706_add_totp_to_users.rb +9 -0
- data/db/migrate/20240825012724_reconfigure_mfa_variable_names.rb +10 -0
- data/lib/rails_base/admin/action_helper.rb +0 -1
- data/lib/rails_base/admin/default_index_tile.rb +3 -3
- data/lib/rails_base/config.rb +26 -22
- data/lib/rails_base/configuration/admin.rb +5 -5
- data/lib/rails_base/configuration/appearance.rb +0 -2
- data/lib/rails_base/configuration/base.rb +1 -0
- data/lib/rails_base/configuration/mfa.rb +27 -60
- data/lib/rails_base/configuration/totp.rb +82 -0
- data/lib/rails_base/configuration/twilio.rb +85 -0
- data/lib/rails_base/mfa_event.rb +186 -0
- data/lib/rails_base/version.rb +3 -3
- data/lib/rails_base.rb +1 -0
- data/lib/twilio_helper.rb +3 -3
- metadata +129 -64
- data/app/controllers/rails_base/mfa_auth_controller.rb +0 -50
- data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +0 -32
- data/app/services/rails_base/authentication/mfa_validator.rb +0 -88
- data/app/views/rails_base/mfa_auth/mfa_code.html.erb +0 -11
- data/app/views/rails_base/secondary_authentication/forgot_password.html.erb +0 -9
@@ -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::
|
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
|
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::
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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::
|
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
|
data/app/jobs/twilio_job.rb
CHANGED
@@ -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
|
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
|
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
|
17
|
-
|
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
|
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
|
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
|
-
|
11
|
+
mfa_sms_enabled: false,
|
12
|
+
mfa_otp_enabled: false,
|
12
13
|
email_validated: false,
|
13
|
-
|
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
|
-
|
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
|
-
#
|
9
|
+
# last_mfa_sms_login :datetime
|
10
10
|
# email_validated :boolean default(FALSE)
|
11
|
-
#
|
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
|
-
####
|
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.
|
69
|
-
|
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
|
81
|
-
|
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
|
87
|
-
update(
|
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
|
-
|
104
|
+
User.masked_number(phone_number)
|
105
|
+
end
|
92
106
|
|
93
|
-
|
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.
|
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
|