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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +32 -0
- data/app/assets/config/rails_base/manifest.js +3 -0
- data/app/assets/images/rails_base/favicon.ico +0 -0
- data/app/assets/javascripts/rails_base/admin.js +2 -0
- data/app/assets/javascripts/rails_base/application.js +22 -0
- data/app/assets/javascripts/rails_base/cable.js +13 -0
- data/app/assets/javascripts/rails_base/mfa_auth.coffee +3 -0
- data/app/assets/javascripts/rails_base/secondary_authentication.coffee +3 -0
- data/app/assets/javascripts/rails_base/sessions.js +152 -0
- data/app/assets/javascripts/rails_base/user_settings.coffee +3 -0
- data/app/assets/stylesheets/rails_base/admin.css +4 -0
- data/app/assets/stylesheets/rails_base/application.scss +15 -0
- data/app/assets/stylesheets/rails_base/mfa_auth.scss +3 -0
- data/app/assets/stylesheets/rails_base/scaffolds.scss +84 -0
- data/app/assets/stylesheets/rails_base/secondary_authentication.scss +3 -0
- data/app/assets/stylesheets/rails_base/user_settings.scss +3 -0
- data/app/controllers/rails_base/admin_controller.rb +315 -0
- data/app/controllers/rails_base/application_controller.rb +153 -0
- data/app/controllers/rails_base/errors_controller.rb +29 -0
- data/app/controllers/rails_base/mfa_auth_controller.rb +50 -0
- data/app/controllers/rails_base/secondary_authentication_controller.rb +224 -0
- data/app/controllers/rails_base/switch_user_controller.rb +29 -0
- data/app/controllers/rails_base/user_settings_controller.rb +81 -0
- data/app/controllers/rails_base/users/passwords_controller.rb +19 -0
- data/app/controllers/rails_base/users/registrations_controller.rb +80 -0
- data/app/controllers/rails_base/users/sessions_controller.rb +108 -0
- data/app/helpers/rails_base/admin_helper.rb +107 -0
- data/app/helpers/rails_base/appearance_helper.rb +58 -0
- data/app/helpers/rails_base/application_helper.rb +26 -0
- data/app/helpers/rails_base/capture_reference_helper.rb +57 -0
- data/app/helpers/rails_base/mfa_auth_helper.rb +2 -0
- data/app/helpers/rails_base/secondary_authentication_helper.rb +2 -0
- data/app/helpers/rails_base/user_field_validators.rb +108 -0
- data/app/helpers/rails_base/user_settings_helper.rb +22 -0
- data/app/jobs/rails_base/application_job.rb +10 -0
- data/app/jobs/twilio_job.rb +9 -0
- data/app/mailers/rails_base/application_mailer.rb +9 -0
- data/app/mailers/rails_base/email_verification_mailer.rb +22 -0
- data/app/mailers/rails_base/event_mailer.rb +16 -0
- data/app/models/admin_action.rb +119 -0
- data/app/models/rails_base/application_record.rb +22 -0
- data/app/models/rails_base/user_constants.rb +28 -0
- data/app/models/secret.rb +37 -0
- data/app/models/short_lived_data.rb +132 -0
- data/app/models/user.rb +143 -0
- data/app/services/rails_base/admin_risky_mfa_send.rb +80 -0
- data/app/services/rails_base/admin_update_attribute.rb +100 -0
- data/app/services/rails_base/authentication/authenticate_user.rb +28 -0
- data/app/services/rails_base/authentication/constants.rb +60 -0
- data/app/services/rails_base/authentication/decision_twofa_type.rb +76 -0
- data/app/services/rails_base/authentication/destroy_user.rb +45 -0
- data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +32 -0
- data/app/services/rails_base/authentication/mfa_validator.rb +88 -0
- data/app/services/rails_base/authentication/modify_password.rb +67 -0
- data/app/services/rails_base/authentication/send_forgot_password.rb +26 -0
- data/app/services/rails_base/authentication/send_login_mfa_to_user.rb +77 -0
- data/app/services/rails_base/authentication/send_verification_email.rb +103 -0
- data/app/services/rails_base/authentication/session_token_verifier.rb +31 -0
- data/app/services/rails_base/authentication/single_sign_on_create.rb +44 -0
- data/app/services/rails_base/authentication/single_sign_on_send.rb +101 -0
- data/app/services/rails_base/authentication/single_sign_on_verify.rb +42 -0
- data/app/services/rails_base/authentication/sso_verify_email.rb +43 -0
- data/app/services/rails_base/authentication/update_phone_send_verification.rb +46 -0
- data/app/services/rails_base/authentication/verify_forgot_password.rb +46 -0
- data/app/services/rails_base/email_change.rb +20 -0
- data/app/services/rails_base/encryption.rb +87 -0
- data/app/services/rails_base/name_change.rb +71 -0
- data/app/services/rails_base/service_base.rb +65 -0
- data/app/services/rails_base/service_logging.rb +23 -0
- data/app/views/layouts/rails_base/application.html.erb +185 -0
- data/app/views/layouts/rails_base/mailer.html.erb +13 -0
- data/app/views/layouts/rails_base/mailer.text.erb +1 -0
- data/app/views/new.html.erb +4 -0
- data/app/views/rails_base/admin/history.html.erb +26 -0
- data/app/views/rails_base/admin/index.html.erb +149 -0
- data/app/views/rails_base/admin/show_config.html.erb +18 -0
- data/app/views/rails_base/devise/confirmations/new.html.erb +16 -0
- data/app/views/rails_base/devise/mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/rails_base/devise/mailer/email_changed.html.erb +7 -0
- data/app/views/rails_base/devise/mailer/password_change.html.erb +3 -0
- data/app/views/rails_base/devise/mailer/reset_password_instructions.html.erb +8 -0
- data/app/views/rails_base/devise/mailer/unlock_instructions.html.erb +7 -0
- data/app/views/rails_base/devise/passwords/edit.html.erb +25 -0
- data/app/views/rails_base/devise/passwords/new.html.erb +27 -0
- data/app/views/rails_base/devise/registrations/edit.html.erb +43 -0
- data/app/views/rails_base/devise/registrations/new.html.erb +123 -0
- data/app/views/rails_base/devise/sessions/new.html.erb +4 -0
- data/app/views/rails_base/devise/shared/_error_messages.html.erb +15 -0
- data/app/views/rails_base/devise/shared/_links.html.erb +25 -0
- data/app/views/rails_base/devise/unlocks/new.html.erb +16 -0
- data/app/views/rails_base/email_verification_mailer/email_verification.html.erb +25 -0
- data/app/views/rails_base/email_verification_mailer/event.html.erb +20 -0
- data/app/views/rails_base/email_verification_mailer/forgot_password.html.erb +22 -0
- data/app/views/rails_base/errors/internal_error.html.erb +1 -0
- data/app/views/rails_base/errors/not_found.html.erb +1 -0
- data/app/views/rails_base/errors/unacceptable.html.erb +1 -0
- data/app/views/rails_base/event_mailer/event.html.erb +10 -0
- data/app/views/rails_base/mfa_auth/mfa_code.html.erb +10 -0
- data/app/views/rails_base/secondary_authentication/after_email_login_session_new.html.erb +3 -0
- data/app/views/rails_base/secondary_authentication/forgot_password.html.erb +9 -0
- data/app/views/rails_base/secondary_authentication/remove_me.html.erb +1 -0
- data/app/views/rails_base/secondary_authentication/static.html.erb +5 -0
- data/app/views/rails_base/shared/_admin_actions_modal.html.erb +65 -0
- data/app/views/rails_base/shared/_admin_config_class.html.erb +52 -0
- data/app/views/rails_base/shared/_admin_history.html.erb +86 -0
- data/app/views/rails_base/shared/_admin_modify_email.html.erb +78 -0
- data/app/views/rails_base/shared/_admin_modify_name.html.erb +107 -0
- data/app/views/rails_base/shared/_admin_modify_phone.html.erb +87 -0
- data/app/views/rails_base/shared/_admin_modify_text.html.erb +35 -0
- data/app/views/rails_base/shared/_admin_risky_change.html.erb +57 -0
- data/app/views/rails_base/shared/_admin_risky_mfa.html.erb +74 -0
- data/app/views/rails_base/shared/_admin_selector_dropdown.html.erb +70 -0
- data/app/views/rails_base/shared/_admin_toggle_button.html.erb +72 -0
- data/app/views/rails_base/shared/_admin_warning_alert.html.erb +7 -0
- data/app/views/rails_base/shared/_appearance_mode_selector.html.erb +183 -0
- data/app/views/rails_base/shared/_custom_form_validation_javascript.html.erb +129 -0
- data/app/views/rails_base/shared/_enable_mfa_auth_modal.html.erb +105 -0
- data/app/views/rails_base/shared/_error_pages.html.erb +123 -0
- data/app/views/rails_base/shared/_logged_in_header.html.erb +123 -0
- data/app/views/rails_base/shared/_logged_out_header.html.erb +14 -0
- data/app/views/rails_base/shared/_mfa_input_layout.html.erb +5 -0
- data/app/views/rails_base/shared/_mfa_input_layout_default.html.erb +97 -0
- data/app/views/rails_base/shared/_mfa_input_layout_fallback.html.erb +55 -0
- data/app/views/rails_base/shared/_modify_mfa_auth_modal.html.erb +20 -0
- data/app/views/rails_base/shared/_password_confirm_javascript.html.erb +71 -0
- data/app/views/rails_base/shared/_reset_password_form.html.erb +111 -0
- data/app/views/rails_base/shared/_session_create_form.html.erb +32 -0
- data/app/views/rails_base/shared/_session_timeout_modal.html.erb +76 -0
- data/app/views/rails_base/switch_user/_widget.html.erb +5 -0
- data/app/views/rails_base/user_settings/_confirm_destroy_user.html.erb +42 -0
- data/app/views/rails_base/user_settings/_destroy_user.html.erb +106 -0
- data/app/views/rails_base/user_settings/_modify_name.html.erb +71 -0
- data/app/views/rails_base/user_settings/_modify_password.html.erb +101 -0
- data/app/views/rails_base/user_settings/_modify_password_update_password.html.erb +2 -0
- data/app/views/rails_base/user_settings/index.html.erb +54 -0
- data/config/initializers/01_rails_config.rb +19 -0
- data/config/initializers/admin_action_helper.rb +88 -0
- data/config/initializers/browser.rb +4 -0
- data/config/initializers/default_logged_in_headers.rb +23 -0
- data/config/initializers/devise.rb +314 -0
- data/config/initializers/encryption.rb +2 -0
- data/config/initializers/switch_user.rb +58 -0
- data/config/initializers/switch_user_helper.rb +29 -0
- data/config/locales/devise.en.yml +65 -0
- data/config/locales/en.yml +58 -0
- data/config/routes.rb +114 -0
- data/db/migrate/20210212175453_devise_create_rails_base_users.rb +56 -0
- data/db/migrate/20210212190537_create_rails_base_short_lived_data.rb +19 -0
- data/db/migrate/20210212192645_create_rails_base_secrets.rb +11 -0
- data/db/migrate/20210406015744_create_rails_base_admin_actions.rb +17 -0
- data/db/seeds.rb +23 -0
- data/lib/link_decision_helper.rb +71 -0
- data/lib/rails_base.rb +50 -0
- data/lib/rails_base/admin/action_cache.rb +99 -0
- data/lib/rails_base/admin/action_helper.rb +134 -0
- data/lib/rails_base/admin/default_index_tile.rb +176 -0
- data/lib/rails_base/admin/index_tile.rb +186 -0
- data/lib/rails_base/config.rb +52 -0
- data/lib/rails_base/configuration/active_job.rb +38 -0
- data/lib/rails_base/configuration/admin.rb +231 -0
- data/lib/rails_base/configuration/app.rb +52 -0
- data/lib/rails_base/configuration/appearance.rb +131 -0
- data/lib/rails_base/configuration/authentication.rb +37 -0
- data/lib/rails_base/configuration/base.rb +209 -0
- data/lib/rails_base/configuration/display/background_color.rb +25 -0
- data/lib/rails_base/configuration/display/btn_danger.rb +25 -0
- data/lib/rails_base/configuration/display/btn_dark.rb +25 -0
- data/lib/rails_base/configuration/display/btn_info.rb +25 -0
- data/lib/rails_base/configuration/display/btn_light.rb +25 -0
- data/lib/rails_base/configuration/display/btn_primary.rb +25 -0
- data/lib/rails_base/configuration/display/btn_secondary.rb +25 -0
- data/lib/rails_base/configuration/display/btn_success.rb +25 -0
- data/lib/rails_base/configuration/display/btn_warning.rb +25 -0
- data/lib/rails_base/configuration/display/footer.rb +54 -0
- data/lib/rails_base/configuration/display/navbar.rb +25 -0
- data/lib/rails_base/configuration/display/table_body.rb +25 -0
- data/lib/rails_base/configuration/display/table_header.rb +25 -0
- data/lib/rails_base/configuration/display/text.rb +26 -0
- data/lib/rails_base/configuration/exceptions_app.rb +25 -0
- data/lib/rails_base/configuration/login_behavior.rb +17 -0
- data/lib/rails_base/configuration/mailer.rb +116 -0
- data/lib/rails_base/configuration/mfa.rb +84 -0
- data/lib/rails_base/configuration/owner.rb +17 -0
- data/lib/rails_base/configuration/redis.rb +29 -0
- data/lib/rails_base/configuration/user.rb +43 -0
- data/lib/rails_base/engine.rb +51 -0
- data/lib/rails_base/version.rb +10 -0
- data/lib/tasks/rails_base_tasks.rake +4 -0
- data/lib/twilio_helper.rb +26 -0
- data/lib/velocity_limiter.rb +91 -0
- 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
|